@thoughtspot/visual-embed-sdk 1.52.1 → 1.52.2-beta.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 (217) hide show
  1. package/cjs/package.json +10 -3
  2. package/cjs/src/contracts/host-event-contracts.d.ts +49 -11
  3. package/cjs/src/contracts/host-event-contracts.d.ts.map +1 -1
  4. package/cjs/src/embed/app.d.ts +4 -3
  5. package/cjs/src/embed/app.d.ts.map +1 -1
  6. package/cjs/src/embed/app.js.map +1 -1
  7. package/cjs/src/embed/app.spec.js +10 -0
  8. package/cjs/src/embed/app.spec.js.map +1 -1
  9. package/cjs/src/embed/auto-frame-renderer.spec.js +24 -13
  10. package/cjs/src/embed/auto-frame-renderer.spec.js.map +1 -1
  11. package/cjs/src/embed/conversation.d.ts +1 -0
  12. package/cjs/src/embed/conversation.d.ts.map +1 -1
  13. package/cjs/src/embed/conversation.js +3 -3
  14. package/cjs/src/embed/conversation.js.map +1 -1
  15. package/cjs/src/embed/conversation.spec.js +35 -24
  16. package/cjs/src/embed/conversation.spec.js.map +1 -1
  17. package/cjs/src/embed/embedConfig.spec.js +2 -1
  18. package/cjs/src/embed/embedConfig.spec.js.map +1 -1
  19. package/cjs/src/embed/hostEventClient/host-event-client.d.ts +2 -2
  20. package/cjs/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  21. package/cjs/src/embed/hostEventClient/host-event-client.js +16 -10
  22. package/cjs/src/embed/hostEventClient/host-event-client.js.map +1 -1
  23. package/cjs/src/embed/hostEventClient/host-event-client.spec.js +14 -8
  24. package/cjs/src/embed/hostEventClient/host-event-client.spec.js.map +1 -1
  25. package/cjs/src/embed/hostEventClient/utils.d.ts +9 -0
  26. package/cjs/src/embed/hostEventClient/utils.d.ts.map +1 -1
  27. package/cjs/src/embed/hostEventClient/utils.js +49 -3
  28. package/cjs/src/embed/hostEventClient/utils.js.map +1 -1
  29. package/cjs/src/embed/hostEventClient/utils.spec.js +151 -0
  30. package/cjs/src/embed/hostEventClient/utils.spec.js.map +1 -1
  31. package/cjs/src/embed/liveboard.d.ts +4 -3
  32. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  33. package/cjs/src/embed/liveboard.js +17 -12
  34. package/cjs/src/embed/liveboard.js.map +1 -1
  35. package/cjs/src/embed/liveboard.spec.js +2 -1
  36. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  37. package/cjs/src/embed/spotter-utils.d.ts +8 -1
  38. package/cjs/src/embed/spotter-utils.d.ts.map +1 -1
  39. package/cjs/src/embed/spotter-utils.js +14 -1
  40. package/cjs/src/embed/spotter-utils.js.map +1 -1
  41. package/cjs/src/embed/spotter-utils.spec.js +26 -0
  42. package/cjs/src/embed/spotter-utils.spec.js.map +1 -1
  43. package/cjs/src/embed/ts-embed.d.ts +35 -10
  44. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  45. package/cjs/src/embed/ts-embed.js +108 -73
  46. package/cjs/src/embed/ts-embed.js.map +1 -1
  47. package/cjs/src/embed/ts-embed.spec.d.ts.map +1 -1
  48. package/cjs/src/embed/ts-embed.spec.js +203 -30
  49. package/cjs/src/embed/ts-embed.spec.js.map +1 -1
  50. package/cjs/src/errors.js +1 -1
  51. package/cjs/src/errors.js.map +1 -1
  52. package/cjs/src/index.d.ts +2 -1
  53. package/cjs/src/index.d.ts.map +1 -1
  54. package/cjs/src/index.js +3 -1
  55. package/cjs/src/index.js.map +1 -1
  56. package/cjs/src/react/all-types-export.d.ts +1 -1
  57. package/cjs/src/react/all-types-export.d.ts.map +1 -1
  58. package/cjs/src/react/all-types-export.js +2 -1
  59. package/cjs/src/react/all-types-export.js.map +1 -1
  60. package/cjs/src/types.d.ts +45 -22
  61. package/cjs/src/types.d.ts.map +1 -1
  62. package/cjs/src/types.js +44 -21
  63. package/cjs/src/types.js.map +1 -1
  64. package/cjs/src/utils/authService/tokenizedAuthService.spec.js +2 -1
  65. package/cjs/src/utils/authService/tokenizedAuthService.spec.js.map +1 -1
  66. package/cjs/src/utils/custom-actions.spec.js +4 -2
  67. package/cjs/src/utils/custom-actions.spec.js.map +1 -1
  68. package/cjs/src/utils/filterConverter.d.ts +106 -0
  69. package/cjs/src/utils/filterConverter.d.ts.map +1 -0
  70. package/cjs/src/utils/filterConverter.js +215 -0
  71. package/cjs/src/utils/filterConverter.js.map +1 -0
  72. package/cjs/src/utils/filterConverter.spec.d.ts +2 -0
  73. package/cjs/src/utils/filterConverter.spec.d.ts.map +1 -0
  74. package/cjs/src/utils/filterConverter.spec.js +709 -0
  75. package/cjs/src/utils/filterConverter.spec.js.map +1 -0
  76. package/cjs/src/utils/processData.spec.js +2 -1
  77. package/cjs/src/utils/processData.spec.js.map +1 -1
  78. package/dist/{index-C1pmYb5H.js → index-BrJMbt8r.js} +1 -1
  79. package/dist/src/contracts/host-event-contracts.d.ts +49 -11
  80. package/dist/src/contracts/host-event-contracts.d.ts.map +1 -1
  81. package/dist/src/embed/app.d.ts +4 -3
  82. package/dist/src/embed/app.d.ts.map +1 -1
  83. package/dist/src/embed/conversation.d.ts +1 -0
  84. package/dist/src/embed/conversation.d.ts.map +1 -1
  85. package/dist/src/embed/hostEventClient/host-event-client.d.ts +2 -2
  86. package/dist/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  87. package/dist/src/embed/hostEventClient/utils.d.ts +9 -0
  88. package/dist/src/embed/hostEventClient/utils.d.ts.map +1 -1
  89. package/dist/src/embed/liveboard.d.ts +4 -3
  90. package/dist/src/embed/liveboard.d.ts.map +1 -1
  91. package/dist/src/embed/spotter-utils.d.ts +8 -1
  92. package/dist/src/embed/spotter-utils.d.ts.map +1 -1
  93. package/dist/src/embed/ts-embed.d.ts +35 -10
  94. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  95. package/dist/src/embed/ts-embed.spec.d.ts.map +1 -1
  96. package/dist/src/index.d.ts +2 -1
  97. package/dist/src/index.d.ts.map +1 -1
  98. package/dist/src/react/all-types-export.d.ts +1 -1
  99. package/dist/src/react/all-types-export.d.ts.map +1 -1
  100. package/dist/src/types.d.ts +45 -22
  101. package/dist/src/types.d.ts.map +1 -1
  102. package/dist/src/utils/filterConverter.d.ts +106 -0
  103. package/dist/src/utils/filterConverter.d.ts.map +1 -0
  104. package/dist/src/utils/filterConverter.spec.d.ts +2 -0
  105. package/dist/src/utils/filterConverter.spec.d.ts.map +1 -0
  106. package/dist/tsembed-react.es.js +252 -126
  107. package/dist/tsembed-react.js +251 -125
  108. package/dist/tsembed.es.js +460 -127
  109. package/dist/tsembed.js +459 -125
  110. package/dist/visual-embed-sdk-react-full.d.ts +233 -49
  111. package/dist/visual-embed-sdk-react.d.ts +139 -49
  112. package/dist/visual-embed-sdk.d.ts +233 -49
  113. package/lib/package.json +10 -3
  114. package/lib/src/contracts/host-event-contracts.d.ts +49 -11
  115. package/lib/src/contracts/host-event-contracts.d.ts.map +1 -1
  116. package/lib/src/embed/app.d.ts +4 -3
  117. package/lib/src/embed/app.d.ts.map +1 -1
  118. package/lib/src/embed/app.js.map +1 -1
  119. package/lib/src/embed/app.spec.js +10 -0
  120. package/lib/src/embed/app.spec.js.map +1 -1
  121. package/lib/src/embed/auto-frame-renderer.spec.js +24 -13
  122. package/lib/src/embed/auto-frame-renderer.spec.js.map +1 -1
  123. package/lib/src/embed/conversation.d.ts +1 -0
  124. package/lib/src/embed/conversation.d.ts.map +1 -1
  125. package/lib/src/embed/conversation.js +4 -4
  126. package/lib/src/embed/conversation.js.map +1 -1
  127. package/lib/src/embed/conversation.spec.js +35 -24
  128. package/lib/src/embed/conversation.spec.js.map +1 -1
  129. package/lib/src/embed/embedConfig.spec.js +2 -1
  130. package/lib/src/embed/embedConfig.spec.js.map +1 -1
  131. package/lib/src/embed/hostEventClient/host-event-client.d.ts +2 -2
  132. package/lib/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  133. package/lib/src/embed/hostEventClient/host-event-client.js +17 -11
  134. package/lib/src/embed/hostEventClient/host-event-client.js.map +1 -1
  135. package/lib/src/embed/hostEventClient/host-event-client.spec.js +14 -8
  136. package/lib/src/embed/hostEventClient/host-event-client.spec.js.map +1 -1
  137. package/lib/src/embed/hostEventClient/utils.d.ts +9 -0
  138. package/lib/src/embed/hostEventClient/utils.d.ts.map +1 -1
  139. package/lib/src/embed/hostEventClient/utils.js +47 -2
  140. package/lib/src/embed/hostEventClient/utils.js.map +1 -1
  141. package/lib/src/embed/hostEventClient/utils.spec.js +152 -1
  142. package/lib/src/embed/hostEventClient/utils.spec.js.map +1 -1
  143. package/lib/src/embed/liveboard.d.ts +4 -3
  144. package/lib/src/embed/liveboard.d.ts.map +1 -1
  145. package/lib/src/embed/liveboard.js +17 -12
  146. package/lib/src/embed/liveboard.js.map +1 -1
  147. package/lib/src/embed/liveboard.spec.js +2 -1
  148. package/lib/src/embed/liveboard.spec.js.map +1 -1
  149. package/lib/src/embed/spotter-utils.d.ts +8 -1
  150. package/lib/src/embed/spotter-utils.d.ts.map +1 -1
  151. package/lib/src/embed/spotter-utils.js +12 -0
  152. package/lib/src/embed/spotter-utils.js.map +1 -1
  153. package/lib/src/embed/spotter-utils.spec.js +27 -1
  154. package/lib/src/embed/spotter-utils.spec.js.map +1 -1
  155. package/lib/src/embed/ts-embed.d.ts +35 -10
  156. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  157. package/lib/src/embed/ts-embed.js +109 -74
  158. package/lib/src/embed/ts-embed.js.map +1 -1
  159. package/lib/src/embed/ts-embed.spec.d.ts.map +1 -1
  160. package/lib/src/embed/ts-embed.spec.js +203 -30
  161. package/lib/src/embed/ts-embed.spec.js.map +1 -1
  162. package/lib/src/errors.js +1 -1
  163. package/lib/src/errors.js.map +1 -1
  164. package/lib/src/index.d.ts +2 -1
  165. package/lib/src/index.d.ts.map +1 -1
  166. package/lib/src/index.js +2 -1
  167. package/lib/src/index.js.map +1 -1
  168. package/lib/src/react/all-types-export.d.ts +1 -1
  169. package/lib/src/react/all-types-export.d.ts.map +1 -1
  170. package/lib/src/react/all-types-export.js +1 -1
  171. package/lib/src/react/all-types-export.js.map +1 -1
  172. package/lib/src/types.d.ts +45 -22
  173. package/lib/src/types.d.ts.map +1 -1
  174. package/lib/src/types.js +44 -21
  175. package/lib/src/types.js.map +1 -1
  176. package/lib/src/utils/authService/tokenizedAuthService.spec.js +2 -1
  177. package/lib/src/utils/authService/tokenizedAuthService.spec.js.map +1 -1
  178. package/lib/src/utils/custom-actions.spec.js +4 -2
  179. package/lib/src/utils/custom-actions.spec.js.map +1 -1
  180. package/lib/src/utils/filterConverter.d.ts +106 -0
  181. package/lib/src/utils/filterConverter.d.ts.map +1 -0
  182. package/lib/src/utils/filterConverter.js +210 -0
  183. package/lib/src/utils/filterConverter.js.map +1 -0
  184. package/lib/src/utils/filterConverter.spec.d.ts +2 -0
  185. package/lib/src/utils/filterConverter.spec.d.ts.map +1 -0
  186. package/lib/src/utils/filterConverter.spec.js +707 -0
  187. package/lib/src/utils/filterConverter.spec.js.map +1 -0
  188. package/lib/src/utils/processData.spec.js +2 -1
  189. package/lib/src/utils/processData.spec.js.map +1 -1
  190. package/lib/src/visual-embed-sdk.d.ts +233 -49
  191. package/package.json +10 -3
  192. package/src/contracts/host-event-contracts.ts +55 -11
  193. package/src/embed/app.spec.ts +15 -0
  194. package/src/embed/app.ts +4 -3
  195. package/src/embed/auto-frame-renderer.spec.ts +24 -13
  196. package/src/embed/conversation.spec.ts +38 -27
  197. package/src/embed/conversation.ts +4 -4
  198. package/src/embed/embedConfig.spec.ts +2 -1
  199. package/src/embed/hostEventClient/host-event-client.spec.ts +14 -8
  200. package/src/embed/hostEventClient/host-event-client.ts +18 -10
  201. package/src/embed/hostEventClient/utils.spec.ts +176 -1
  202. package/src/embed/hostEventClient/utils.ts +51 -2
  203. package/src/embed/liveboard.spec.ts +4 -3
  204. package/src/embed/liveboard.ts +21 -15
  205. package/src/embed/spotter-utils.spec.ts +29 -0
  206. package/src/embed/spotter-utils.ts +16 -0
  207. package/src/embed/ts-embed.spec.ts +245 -30
  208. package/src/embed/ts-embed.ts +107 -74
  209. package/src/errors.ts +1 -1
  210. package/src/index.ts +10 -0
  211. package/src/react/all-types-export.ts +4 -0
  212. package/src/types.ts +44 -21
  213. package/src/utils/authService/tokenizedAuthService.spec.ts +2 -1
  214. package/src/utils/custom-actions.spec.ts +4 -2
  215. package/src/utils/filterConverter.spec.ts +801 -0
  216. package/src/utils/filterConverter.ts +304 -0
  217. package/src/utils/processData.spec.ts +2 -1
@@ -0,0 +1,801 @@
1
+ import { RuntimeFilterOp } from '../types';
2
+ import { isValidUpdateFiltersPayload } from '../embed/hostEventClient/utils';
3
+ import { convertFilterChangedToUpdateFiltersPayload, FilterChangedPayload } from './filterConverter';
4
+
5
+ describe('convertFilterChangedToUpdateFiltersPayload', () => {
6
+ test('returns an empty filters array for an empty payload', () => {
7
+ expect(convertFilterChangedToUpdateFiltersPayload({})).toEqual({ filters: [] });
8
+ });
9
+
10
+ test('returns an empty filters array for null/undefined input', () => {
11
+ expect(convertFilterChangedToUpdateFiltersPayload(null as any)).toEqual({ filters: [] });
12
+ expect(convertFilterChangedToUpdateFiltersPayload(undefined as any)).toEqual({ filters: [] });
13
+ });
14
+
15
+ test('converts runtime filters as-is', () => {
16
+ const payload: FilterChangedPayload = {
17
+ runtimeFilters: [
18
+ { columnName: 'state', operator: RuntimeFilterOp.EQ, values: ['california'] },
19
+ ],
20
+ };
21
+
22
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
23
+ filters: [
24
+ { columnName: 'state', operator: RuntimeFilterOp.EQ, values: ['california'] },
25
+ ],
26
+ });
27
+ });
28
+
29
+ test('converts a simple attribute filter (filterContent)', () => {
30
+ const payload: FilterChangedPayload = {
31
+ liveboardFilters: [
32
+ {
33
+ columnInfo: { name: 'item type' },
34
+ filters: [
35
+ {
36
+ filterContent: [
37
+ {
38
+ filterType: 'IN',
39
+ value: [{ key: 'bags' }, { key: 'shirts' }],
40
+ },
41
+ ],
42
+ },
43
+ ],
44
+ },
45
+ ],
46
+ };
47
+
48
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
49
+ filters: [
50
+ { columnName: 'item type', operator: 'IN', values: ['bags', 'shirts'] },
51
+ ],
52
+ });
53
+ });
54
+
55
+ test('includes negate when set on a filterContent filter', () => {
56
+ const payload: FilterChangedPayload = {
57
+ liveboardFilters: [
58
+ {
59
+ columnInfo: { name: 'region' },
60
+ filters: [
61
+ {
62
+ filterContent: [
63
+ {
64
+ filterType: 'EQ',
65
+ negate: true,
66
+ value: [{ key: 'west' }],
67
+ },
68
+ ],
69
+ },
70
+ ],
71
+ },
72
+ ],
73
+ };
74
+
75
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
76
+ filters: [
77
+ { columnName: 'region', operator: 'EQ', values: ['west'], negate: true },
78
+ ],
79
+ });
80
+ });
81
+
82
+ test('skips a filter group with no columnInfo.name', () => {
83
+ const payload: FilterChangedPayload = {
84
+ liveboardFilters: [
85
+ {
86
+ filters: [
87
+ { filterContent: [{ filterType: 'EQ', value: [{ key: 'x' }] }] },
88
+ ],
89
+ },
90
+ ],
91
+ };
92
+
93
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({ filters: [] });
94
+ });
95
+
96
+ test('skips a filter with neither filterContent nor dateFilterContent', () => {
97
+ const payload: FilterChangedPayload = {
98
+ liveboardFilters: [
99
+ {
100
+ columnInfo: { name: 'region' },
101
+ filters: [{}],
102
+ },
103
+ ],
104
+ };
105
+
106
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({ filters: [] });
107
+ });
108
+
109
+ test('converts an EXACT_DATE filter using epoch', () => {
110
+ const payload: FilterChangedPayload = {
111
+ liveboardFilters: [
112
+ {
113
+ columnInfo: { name: 'date' },
114
+ filters: [
115
+ {
116
+ dateFilterContent: [
117
+ {
118
+ dateFilter: { type: 'EXACT_DATE', op: 'EQ', epoch: '1690847400' },
119
+ },
120
+ ],
121
+ },
122
+ ],
123
+ },
124
+ ],
125
+ };
126
+
127
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
128
+ filters: [
129
+ {
130
+ columnName: 'date', operator: 'EQ', values: [1690847400], type: 'EXACT_DATE',
131
+ },
132
+ ],
133
+ });
134
+ });
135
+
136
+ test('converts an EXACT_DATE_RANGE filter using dateRange epochs', () => {
137
+ const payload: FilterChangedPayload = {
138
+ liveboardFilters: [
139
+ {
140
+ columnInfo: { name: 'date' },
141
+ filters: [
142
+ {
143
+ dateFilterContent: [
144
+ {
145
+ dateFilter: {
146
+ type: 'EXACT_DATE_RANGE',
147
+ op: 'BW_INC',
148
+ dateRange: { lowEpoch: '100', highEpoch: '200' },
149
+ },
150
+ },
151
+ ],
152
+ },
153
+ ],
154
+ },
155
+ ],
156
+ };
157
+
158
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
159
+ filters: [
160
+ {
161
+ columnName: 'date', operator: 'BW_INC', values: [100, 200], type: 'EXACT_DATE_RANGE',
162
+ },
163
+ ],
164
+ });
165
+ });
166
+
167
+ test('converts a MONTH_YEAR filter', () => {
168
+ const payload: FilterChangedPayload = {
169
+ liveboardFilters: [
170
+ {
171
+ columnInfo: { name: 'date' },
172
+ filters: [
173
+ {
174
+ dateFilterContent: [
175
+ {
176
+ dateFilter: {
177
+ type: 'MONTH_YEAR', op: 'EQ', monthName: 'JULY', yearName: '2023',
178
+ },
179
+ },
180
+ ],
181
+ },
182
+ ],
183
+ },
184
+ ],
185
+ };
186
+
187
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
188
+ filters: [
189
+ {
190
+ columnName: 'date', operator: 'EQ', values: ['JULY', '2023'], type: 'MONTH_YEAR',
191
+ },
192
+ ],
193
+ });
194
+ });
195
+
196
+ test('converts a QUARTER_YEAR filter', () => {
197
+ const payload: FilterChangedPayload = {
198
+ liveboardFilters: [
199
+ {
200
+ columnInfo: { name: 'date' },
201
+ filters: [
202
+ {
203
+ dateFilterContent: [
204
+ {
205
+ dateFilter: {
206
+ type: 'QUARTER_YEAR', op: 'EQ', quarterName: 'Q1', yearName: '2023',
207
+ },
208
+ },
209
+ ],
210
+ },
211
+ ],
212
+ },
213
+ ],
214
+ };
215
+
216
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
217
+ filters: [
218
+ {
219
+ columnName: 'date', operator: 'EQ', values: ['Q1', '2023'], type: 'QUARTER_YEAR',
220
+ },
221
+ ],
222
+ });
223
+ });
224
+
225
+ test('converts a YEAR_ONLY filter', () => {
226
+ const payload: FilterChangedPayload = {
227
+ liveboardFilters: [
228
+ {
229
+ columnInfo: { name: 'date' },
230
+ filters: [
231
+ {
232
+ dateFilterContent: [
233
+ { dateFilter: { type: 'YEAR_ONLY', op: 'EQ', yearName: '2023' } },
234
+ ],
235
+ },
236
+ ],
237
+ },
238
+ ],
239
+ };
240
+
241
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
242
+ filters: [
243
+ { columnName: 'date', operator: 'EQ', values: ['2023'], type: 'YEAR_ONLY' },
244
+ ],
245
+ });
246
+ });
247
+
248
+ test('converts a LAST_N_PERIOD filter with datePeriod and includeCurrentPeriod', () => {
249
+ const payload: FilterChangedPayload = {
250
+ liveboardFilters: [
251
+ {
252
+ columnInfo: { name: 'date' },
253
+ filters: [
254
+ {
255
+ dateFilterContent: [
256
+ {
257
+ negate: true,
258
+ dateFilter: {
259
+ type: 'LAST_N_PERIOD',
260
+ op: 'EQ',
261
+ number: 3,
262
+ datePeriod: 'MONTH',
263
+ includeCurrentPeriod: true,
264
+ },
265
+ },
266
+ ],
267
+ },
268
+ ],
269
+ },
270
+ ],
271
+ };
272
+
273
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
274
+ filters: [
275
+ {
276
+ columnName: 'date',
277
+ operator: 'EQ',
278
+ values: [3],
279
+ type: 'LAST_N_PERIOD',
280
+ datePeriod: 'MONTH',
281
+ includeCurrentPeriod: true,
282
+ negate: true,
283
+ },
284
+ ],
285
+ });
286
+ });
287
+
288
+ test('converts a period-only date filter (e.g. TODAY) with empty values', () => {
289
+ const payload: FilterChangedPayload = {
290
+ liveboardFilters: [
291
+ {
292
+ columnInfo: { name: 'date' },
293
+ filters: [
294
+ {
295
+ dateFilterContent: [
296
+ { dateFilter: { type: 'TODAY', op: 'EQ' } },
297
+ ],
298
+ },
299
+ ],
300
+ },
301
+ ],
302
+ };
303
+
304
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
305
+ filters: [
306
+ { columnName: 'date', operator: 'EQ', values: [], type: 'TODAY' },
307
+ ],
308
+ });
309
+ });
310
+
311
+ test('skips a dateFilterContent entry with no type', () => {
312
+ const payload: FilterChangedPayload = {
313
+ liveboardFilters: [
314
+ {
315
+ columnInfo: { name: 'date' },
316
+ filters: [
317
+ { dateFilterContent: [{ dateFilter: {} }] },
318
+ ],
319
+ },
320
+ ],
321
+ };
322
+
323
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({ filters: [] });
324
+ });
325
+
326
+ test('defaults operator to EQ when dateFilter.op is missing', () => {
327
+ const payload: FilterChangedPayload = {
328
+ liveboardFilters: [
329
+ {
330
+ columnInfo: { name: 'date' },
331
+ filters: [
332
+ { dateFilterContent: [{ dateFilter: { type: 'TODAY' } }] },
333
+ ],
334
+ },
335
+ ],
336
+ };
337
+
338
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
339
+ filters: [
340
+ { columnName: 'date', operator: RuntimeFilterOp.EQ, values: [], type: 'TODAY' },
341
+ ],
342
+ });
343
+ });
344
+
345
+ test('converts every filterContent entry on a filter, not just the first', () => {
346
+ const payload: FilterChangedPayload = {
347
+ liveboardFilters: [
348
+ {
349
+ columnInfo: { name: 'quantity' },
350
+ filters: [
351
+ {
352
+ filterContent: [
353
+ { filterType: 'GE', value: [{ key: 5 }] },
354
+ { filterType: 'LE', value: [{ key: 10 }] },
355
+ ],
356
+ },
357
+ ],
358
+ },
359
+ ],
360
+ };
361
+
362
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
363
+ filters: [
364
+ { columnName: 'quantity', operator: 'GE', values: [5] },
365
+ { columnName: 'quantity', operator: 'LE', values: [10] },
366
+ ],
367
+ });
368
+ });
369
+
370
+ test('converts every Filter entry in a filter group, not just the first', () => {
371
+ const payload: FilterChangedPayload = {
372
+ liveboardFilters: [
373
+ {
374
+ columnInfo: { name: 'region' },
375
+ filters: [
376
+ { filterContent: [{ filterType: 'EQ', value: [{ key: 'west' }] }] },
377
+ { filterContent: [{ filterType: 'EQ', value: [{ key: 'east' }] }] },
378
+ ],
379
+ },
380
+ ],
381
+ };
382
+
383
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
384
+ filters: [
385
+ { columnName: 'region', operator: 'EQ', values: ['west'] },
386
+ { columnName: 'region', operator: 'EQ', values: ['east'] },
387
+ ],
388
+ });
389
+ });
390
+
391
+ test('keeps falsy-but-defined filter values such as 0 and false', () => {
392
+ const payload: FilterChangedPayload = {
393
+ liveboardFilters: [
394
+ {
395
+ columnInfo: { name: 'flag' },
396
+ filters: [
397
+ {
398
+ filterContent: [
399
+ { filterType: 'IN', value: [{ key: 0 }, { key: false }] },
400
+ ],
401
+ },
402
+ ],
403
+ },
404
+ ],
405
+ };
406
+
407
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
408
+ filters: [
409
+ { columnName: 'flag', operator: 'IN', values: [0, false] },
410
+ ],
411
+ });
412
+ });
413
+
414
+ test('skips an EXACT_DATE filter missing its epoch instead of emitting empty values', () => {
415
+ const payload: FilterChangedPayload = {
416
+ liveboardFilters: [
417
+ {
418
+ columnInfo: { name: 'date' },
419
+ filters: [
420
+ { dateFilterContent: [{ dateFilter: { type: 'EXACT_DATE', op: 'EQ' } }] },
421
+ ],
422
+ },
423
+ ],
424
+ };
425
+
426
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({ filters: [] });
427
+ });
428
+
429
+ test('skips a MONTH_YEAR filter missing yearName instead of emitting empty values', () => {
430
+ const payload: FilterChangedPayload = {
431
+ liveboardFilters: [
432
+ {
433
+ columnInfo: { name: 'date' },
434
+ filters: [
435
+ {
436
+ dateFilterContent: [
437
+ { dateFilter: { type: 'MONTH_YEAR', op: 'EQ', monthName: 'JULY' } },
438
+ ],
439
+ },
440
+ ],
441
+ },
442
+ ],
443
+ };
444
+
445
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({ filters: [] });
446
+ });
447
+
448
+ test('skips an unrecognized date filter type rather than guessing', () => {
449
+ const payload: FilterChangedPayload = {
450
+ liveboardFilters: [
451
+ {
452
+ columnInfo: { name: 'date' },
453
+ filters: [
454
+ { dateFilterContent: [{ dateFilter: { type: 'SOME_FUTURE_TYPE', op: 'EQ' } }] },
455
+ ],
456
+ },
457
+ ],
458
+ };
459
+
460
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({ filters: [] });
461
+ });
462
+
463
+ test('output satisfies isValidUpdateFiltersPayload', () => {
464
+ const payload: FilterChangedPayload = {
465
+ liveboardFilters: [
466
+ {
467
+ columnInfo: { name: 'item type' },
468
+ filters: [
469
+ { filterContent: [{ filterType: 'IN', value: [{ key: 'bags' }] }] },
470
+ ],
471
+ },
472
+ ],
473
+ runtimeFilters: [
474
+ { columnName: 'region', operator: RuntimeFilterOp.EQ, values: ['west'] },
475
+ ],
476
+ };
477
+
478
+ const converted = convertFilterChangedToUpdateFiltersPayload(payload);
479
+ expect(isValidUpdateFiltersPayload(converted as any)).toBe(true);
480
+ });
481
+
482
+ test('combines multiple liveboard filters and runtime filters', () => {
483
+ const payload: FilterChangedPayload = {
484
+ liveboardFilters: [
485
+ {
486
+ columnInfo: { name: 'item type' },
487
+ filters: [
488
+ { filterContent: [{ filterType: 'IN', value: [{ key: 'bags' }] }] },
489
+ ],
490
+ },
491
+ ],
492
+ runtimeFilters: [
493
+ { columnName: 'region', operator: RuntimeFilterOp.EQ, values: ['west'] },
494
+ ],
495
+ };
496
+
497
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
498
+ filters: [
499
+ { columnName: 'item type', operator: 'IN', values: ['bags'] },
500
+ { columnName: 'region', operator: RuntimeFilterOp.EQ, values: ['west'] },
501
+ ],
502
+ });
503
+ });
504
+ // The payload arrives as JSON from the embedded app, so an absent field can
505
+ // be `null` rather than omitted. `Number(null)` is 0, so a naive coercion
506
+ // would turn a missing epoch into 1970 and a missing period count into 0.
507
+ describe('null and non-numeric values in the payload', () => {
508
+ const dateFilterPayload = (dateFilter: Record<string, unknown>): FilterChangedPayload => ({
509
+ liveboardFilters: [
510
+ {
511
+ columnInfo: { name: 'date' },
512
+ filters: [{ dateFilterContent: [{ dateFilter: dateFilter as any }] }],
513
+ },
514
+ ],
515
+ });
516
+
517
+ test.each([null, '', 'not-a-date'])(
518
+ 'skips an EXACT_DATE filter whose epoch is %p instead of emitting epoch 0',
519
+ (epoch) => {
520
+ const payload = dateFilterPayload({ type: 'EXACT_DATE', op: 'EQ', epoch });
521
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({ filters: [] });
522
+ },
523
+ );
524
+
525
+ test('skips an EXACT_DATE_RANGE filter when either bound is null', () => {
526
+ const missingHigh = dateFilterPayload({
527
+ type: 'EXACT_DATE_RANGE', op: 'BW', dateRange: { lowEpoch: 1000, highEpoch: null },
528
+ });
529
+ const missingLow = dateFilterPayload({
530
+ type: 'EXACT_DATE_RANGE', op: 'BW', dateRange: { lowEpoch: null, highEpoch: 2000 },
531
+ });
532
+
533
+ expect(convertFilterChangedToUpdateFiltersPayload(missingHigh)).toEqual({ filters: [] });
534
+ expect(convertFilterChangedToUpdateFiltersPayload(missingLow)).toEqual({ filters: [] });
535
+ });
536
+
537
+ test.each(['LAST_N_PERIOD', 'NEXT_N_PERIOD'])(
538
+ 'skips a %s filter whose number is null instead of emitting a 0-length period',
539
+ (type) => {
540
+ const payload = dateFilterPayload({ type, op: 'EQ', number: null, datePeriod: 'MONTH' });
541
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({ filters: [] });
542
+ },
543
+ );
544
+
545
+ test('accepts an epoch of 0 as a real value', () => {
546
+ const payload = dateFilterPayload({ type: 'EXACT_DATE', op: 'EQ', epoch: 0 });
547
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
548
+ filters: [{
549
+ columnName: 'date', operator: 'EQ', values: [0], type: 'EXACT_DATE',
550
+ }],
551
+ });
552
+ });
553
+
554
+ test('accepts a numeric epoch sent as a string', () => {
555
+ const payload = dateFilterPayload({ type: 'EXACT_DATE', op: 'EQ', epoch: '1700000000' });
556
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
557
+ filters: [{
558
+ columnName: 'date', operator: 'EQ', values: [1700000000], type: 'EXACT_DATE',
559
+ }],
560
+ });
561
+ });
562
+
563
+ test('omits includeCurrentPeriod when it is null', () => {
564
+ const payload = dateFilterPayload({
565
+ type: 'LAST_N_PERIOD', op: 'EQ', number: 3, datePeriod: 'MONTH', includeCurrentPeriod: null,
566
+ });
567
+ const [filter] = convertFilterChangedToUpdateFiltersPayload(payload).filters;
568
+
569
+ expect(filter).not.toHaveProperty('includeCurrentPeriod');
570
+ });
571
+
572
+ test('keeps includeCurrentPeriod when it is explicitly false', () => {
573
+ const payload = dateFilterPayload({
574
+ type: 'LAST_N_PERIOD', op: 'EQ', number: 3, datePeriod: 'MONTH', includeCurrentPeriod: false,
575
+ });
576
+ const [filter] = convertFilterChangedToUpdateFiltersPayload(payload).filters;
577
+
578
+ expect(filter.includeCurrentPeriod).toBe(false);
579
+ });
580
+
581
+ test('drops null keys from an attribute filter but keeps the rest', () => {
582
+ const payload: FilterChangedPayload = {
583
+ liveboardFilters: [
584
+ {
585
+ columnInfo: { name: 'item type' },
586
+ filters: [
587
+ {
588
+ filterContent: [
589
+ {
590
+ filterType: 'IN',
591
+ value: [{ key: 'bags' }, { key: null }, { key: 'shirts' }],
592
+ },
593
+ ],
594
+ },
595
+ ],
596
+ },
597
+ ],
598
+ };
599
+
600
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
601
+ filters: [{ columnName: 'item type', operator: 'IN', values: ['bags', 'shirts'] }],
602
+ });
603
+ });
604
+
605
+ test('keeps a falsy-but-real key such as 0 or false', () => {
606
+ const payload: FilterChangedPayload = {
607
+ liveboardFilters: [
608
+ {
609
+ columnInfo: { name: 'quantity' },
610
+ filters: [
611
+ { filterContent: [{ filterType: 'IN', value: [{ key: 0 }, { key: false }] }] },
612
+ ],
613
+ },
614
+ ],
615
+ };
616
+
617
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
618
+ filters: [{ columnName: 'quantity', operator: 'IN', values: [0, false] }],
619
+ });
620
+ });
621
+ });
622
+ // A runtime filter needs no reshaping, but it still arrives as JSON. One
623
+ // malformed entry fails isValidUpdateFiltersPayload for the WHOLE filters
624
+ // array, which would take every other converted filter down with it.
625
+ describe('malformed runtime filters', () => {
626
+ const malformed: Record<string, unknown>[] = [
627
+ { columnName: null, operator: RuntimeFilterOp.EQ, values: ['west'] },
628
+ { columnName: '', operator: RuntimeFilterOp.EQ, values: ['west'] },
629
+ { operator: RuntimeFilterOp.EQ, values: ['west'] },
630
+ { columnName: 'region', operator: null, values: ['west'] },
631
+ { columnName: 'region', values: ['west'] },
632
+ { columnName: 'region', operator: RuntimeFilterOp.EQ, values: null },
633
+ { columnName: 'region', operator: RuntimeFilterOp.EQ },
634
+ ];
635
+
636
+ test.each(malformed)('skips the malformed runtime filter %p', (runtimeFilter) => {
637
+ const payload = { runtimeFilters: [runtimeFilter] } as any as FilterChangedPayload;
638
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({ filters: [] });
639
+ });
640
+
641
+ test('a bad runtime filter does not discard the good filters alongside it', () => {
642
+ const payload = {
643
+ liveboardFilters: [
644
+ {
645
+ columnInfo: { name: 'item type' },
646
+ filters: [{ filterContent: [{ filterType: 'IN', value: [{ key: 'bags' }] }] }],
647
+ },
648
+ ],
649
+ runtimeFilters: [
650
+ { columnName: null, operator: RuntimeFilterOp.EQ, values: ['west'] },
651
+ { columnName: 'region', operator: RuntimeFilterOp.EQ, values: ['west'] },
652
+ ],
653
+ } as any as FilterChangedPayload;
654
+
655
+ const converted = convertFilterChangedToUpdateFiltersPayload(payload);
656
+
657
+ expect(converted).toEqual({
658
+ filters: [
659
+ { columnName: 'item type', operator: 'IN', values: ['bags'] },
660
+ { columnName: 'region', operator: RuntimeFilterOp.EQ, values: ['west'] },
661
+ ],
662
+ });
663
+ // The whole point: the surviving payload is still triggerable.
664
+ expect(isValidUpdateFiltersPayload(converted as any)).toBe(true);
665
+ });
666
+
667
+ test('keeps a runtime filter with an empty values array', () => {
668
+ const payload: FilterChangedPayload = {
669
+ runtimeFilters: [{ columnName: 'region', operator: RuntimeFilterOp.EQ, values: [] }],
670
+ };
671
+
672
+ expect(convertFilterChangedToUpdateFiltersPayload(payload)).toEqual({
673
+ filters: [{ columnName: 'region', operator: RuntimeFilterOp.EQ, values: [] }],
674
+ });
675
+ });
676
+ });
677
+
678
+ // liveboardFilters entries carry an optional `applicability`
679
+ // ({level, targetId}) from SDK 1.53.0 / 26.10.0.cl, and UpdateFilters
680
+ // accepts it per filter. Dropping it would replay a tab-scoped filter
681
+ // across the whole Liveboard.
682
+ describe('applicability (filter scope)', () => {
683
+ const TAB_ID = 'e0836cad-4fdf-42d4-bd97-567a6b2a6058';
684
+
685
+ const groupWith = (applicability?: unknown) => ({
686
+ liveboardFilters: [
687
+ {
688
+ columnInfo: { name: 'item type' },
689
+ applicability,
690
+ filters: [{ filterContent: [{ filterType: 'IN', value: [{ key: 'bags' }] }] }],
691
+ },
692
+ ],
693
+ } as any as FilterChangedPayload);
694
+
695
+ test('carries TAB scope through to the converted filter', () => {
696
+ const converted = convertFilterChangedToUpdateFiltersPayload(
697
+ groupWith({ level: 'TAB', targetId: TAB_ID }),
698
+ );
699
+
700
+ expect(converted).toEqual({
701
+ filters: [{
702
+ columnName: 'item type',
703
+ operator: 'IN',
704
+ values: ['bags'],
705
+ applicability: { level: 'TAB', targetId: TAB_ID },
706
+ }],
707
+ });
708
+ expect(isValidUpdateFiltersPayload(converted as any)).toBe(true);
709
+ });
710
+
711
+ test('carries GROUP scope through to the converted filter', () => {
712
+ const converted = convertFilterChangedToUpdateFiltersPayload(
713
+ groupWith({ level: 'GROUP', targetId: TAB_ID }),
714
+ );
715
+
716
+ expect(converted.filters[0].applicability).toEqual({ level: 'GROUP', targetId: TAB_ID });
717
+ expect(isValidUpdateFiltersPayload(converted as any)).toBe(true);
718
+ });
719
+
720
+ test('carries LIVEBOARD scope, which needs no targetId', () => {
721
+ const converted = convertFilterChangedToUpdateFiltersPayload(
722
+ groupWith({ level: 'LIVEBOARD' }),
723
+ );
724
+
725
+ expect(converted.filters[0].applicability).toEqual({ level: 'LIVEBOARD' });
726
+ expect(isValidUpdateFiltersPayload(converted as any)).toBe(true);
727
+ });
728
+
729
+ test('omits applicability entirely when the payload has none', () => {
730
+ const converted = convertFilterChangedToUpdateFiltersPayload(groupWith(undefined));
731
+
732
+ expect(converted.filters[0]).not.toHaveProperty('applicability');
733
+ expect(isValidUpdateFiltersPayload(converted as any)).toBe(true);
734
+ });
735
+
736
+ test('applies the group scope to every filter the group produces', () => {
737
+ const payload = {
738
+ liveboardFilters: [
739
+ {
740
+ columnInfo: { name: 'quantity' },
741
+ applicability: { level: 'TAB', targetId: TAB_ID },
742
+ filters: [
743
+ {
744
+ filterContent: [
745
+ { filterType: 'GE', value: [{ key: 5 }] },
746
+ { filterType: 'LE', value: [{ key: 10 }] },
747
+ ],
748
+ },
749
+ ],
750
+ },
751
+ ],
752
+ } as any as FilterChangedPayload;
753
+
754
+ const converted = convertFilterChangedToUpdateFiltersPayload(payload);
755
+
756
+ expect(converted.filters).toHaveLength(2);
757
+ converted.filters.forEach((filter) => {
758
+ expect(filter.applicability).toEqual({ level: 'TAB', targetId: TAB_ID });
759
+ });
760
+ });
761
+
762
+ test.each([
763
+ [{ level: 'TAB' }, 'TAB with no targetId'],
764
+ [{ level: 'GROUP', targetId: ' ' }, 'GROUP with a blank targetId'],
765
+ [{ level: 'SOMETHING_ELSE', targetId: TAB_ID }, 'an unknown level'],
766
+ [{ targetId: TAB_ID }, 'no level at all'],
767
+ ])('skips a filter scoped with %p (%s) rather than widening it', (applicability, _label) => {
768
+ expect(convertFilterChangedToUpdateFiltersPayload(groupWith(applicability)))
769
+ .toEqual({ filters: [] });
770
+ });
771
+
772
+ test('a malformed scope on one column does not invalidate the others', () => {
773
+ const payload = {
774
+ liveboardFilters: [
775
+ {
776
+ columnInfo: { name: 'item type' },
777
+ applicability: { level: 'TAB' },
778
+ filters: [{ filterContent: [{ filterType: 'IN', value: [{ key: 'bags' }] }] }],
779
+ },
780
+ {
781
+ columnInfo: { name: 'region' },
782
+ applicability: { level: 'TAB', targetId: TAB_ID },
783
+ filters: [{ filterContent: [{ filterType: 'IN', value: [{ key: 'west' }] }] }],
784
+ },
785
+ ],
786
+ } as any as FilterChangedPayload;
787
+
788
+ const converted = convertFilterChangedToUpdateFiltersPayload(payload);
789
+
790
+ expect(converted).toEqual({
791
+ filters: [{
792
+ columnName: 'region',
793
+ operator: 'IN',
794
+ values: ['west'],
795
+ applicability: { level: 'TAB', targetId: TAB_ID },
796
+ }],
797
+ });
798
+ expect(isValidUpdateFiltersPayload(converted as any)).toBe(true);
799
+ });
800
+ });
801
+ });