@servicetitan/titan-chatbot-api 4.4.0 → 4.4.2

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.
@@ -0,0 +1,453 @@
1
+ import { expect } from '@jest/globals';
2
+ import { Models, ModelsMocks } from '../..';
3
+ import { createSelectionsModel } from '../model-utils';
4
+
5
+ describe('[model-utils] createSelectionsModel', () => {
6
+ afterEach(() => {
7
+ jest.clearAllMocks();
8
+ });
9
+
10
+ describe('with no selections', () => {
11
+ test('should return undefined when no selections are made', () => {
12
+ const frontendModel = ModelsMocks.mockFrontendModel();
13
+ const filters = [frontendModel.options];
14
+ const selected = new Map<string, string[]>();
15
+
16
+ const result = createSelectionsModel(filters, selected);
17
+
18
+ expect(result).toBeUndefined();
19
+ });
20
+
21
+ test('should return undefined when selected map is empty', () => {
22
+ const frontendModel = ModelsMocks.mockFrontendModel();
23
+ const filters = [frontendModel.options];
24
+ const selected = new Map<string, string[]>();
25
+
26
+ const result = createSelectionsModel(filters, selected);
27
+
28
+ expect(result).toBeUndefined();
29
+ });
30
+
31
+ test('should handle empty selection arrays', () => {
32
+ const frontendModel = ModelsMocks.mockFrontendModel();
33
+ const filters = [frontendModel.options];
34
+ const selected = new Map<string, string[]>([
35
+ ['Sources', []],
36
+ ['ContentTypes', []],
37
+ ]);
38
+
39
+ const result = createSelectionsModel(filters, selected);
40
+
41
+ expect(result).toBeUndefined();
42
+ });
43
+ });
44
+
45
+ describe('with flat filter structure (leaf filters only)', () => {
46
+ test('top level, single filter, single value', () => {
47
+ const flatModel = ModelsMocks.mockFrontendModelFlat();
48
+ const flatFilters = flatModel.options.subOptions ?? [];
49
+ const selected = new Map<string, string[]>([['ContentTypes', ['kbReleaseNotes']]]);
50
+
51
+ const result = createSelectionsModel(flatFilters, selected);
52
+ expect(result).toEqual({
53
+ subOptions: {
54
+ ContentTypes: {
55
+ values: ['kbReleaseNotes'],
56
+ },
57
+ },
58
+ });
59
+ });
60
+
61
+ test('top level, single filter, multiple values', () => {
62
+ const flatModel = ModelsMocks.mockFrontendModelFlat();
63
+ const flatFilters = flatModel.options.subOptions ?? [];
64
+ const selected = new Map<string, string[]>([
65
+ ['ContentTypes', ['kbReleaseNotes', 'kbFaq', 'kbHowTo']],
66
+ ]);
67
+
68
+ const result = createSelectionsModel(flatFilters, selected);
69
+ expect(result).toEqual({
70
+ subOptions: {
71
+ ContentTypes: {
72
+ values: ['kbReleaseNotes', 'kbFaq', 'kbHowTo'],
73
+ },
74
+ },
75
+ });
76
+ });
77
+
78
+ test('top level, multiple filter, multiple values', () => {
79
+ const flatModel = ModelsMocks.mockFrontendModelFlat();
80
+ const flatFilters = flatModel.options.subOptions ?? [];
81
+ const selected = new Map<string, string[]>([
82
+ ['ContentTypes', ['kbReleaseNotes', 'kbFaq']],
83
+ ['ProductAreas', ['Marketing']],
84
+ ]);
85
+
86
+ const result = createSelectionsModel(flatFilters, selected);
87
+ expect(result).toEqual({
88
+ subOptions: {
89
+ ContentTypes: {
90
+ values: ['kbReleaseNotes', 'kbFaq'],
91
+ },
92
+ ProductAreas: {
93
+ values: ['Marketing'],
94
+ },
95
+ },
96
+ });
97
+ });
98
+
99
+ test('non-existing filter', () => {
100
+ const flatModel = ModelsMocks.mockFrontendModelFlat();
101
+ const flatFilters = flatModel.options.subOptions ?? [];
102
+ const selected = new Map<string, string[]>([
103
+ ['ContentTypes', ['kbReleaseNotes', 'nonExistentOption']],
104
+ ]);
105
+
106
+ const result = createSelectionsModel(flatFilters, selected);
107
+ expect(result).toEqual({
108
+ subOptions: {
109
+ ContentTypes: {
110
+ values: ['kbReleaseNotes'],
111
+ },
112
+ },
113
+ });
114
+ });
115
+
116
+ test('no valid filters', () => {
117
+ const flatModel = ModelsMocks.mockFrontendModelFlat();
118
+ const flatFilters = flatModel.options.subOptions ?? [];
119
+ const selected = new Map<string, string[]>([
120
+ ['ContentTypes', ['nonExistent1', 'nonExistent2']],
121
+ ]);
122
+
123
+ const result = createSelectionsModel(flatFilters, selected);
124
+ expect(result).toBeUndefined();
125
+ });
126
+ });
127
+
128
+ describe('with mixed filter structure', () => {
129
+ test('top level, single filter, single value', () => {
130
+ const frontendModel = ModelsMocks.mockFrontendModelMixed();
131
+ const filters = frontendModel.options.subOptions ?? [];
132
+ const selected = new Map<string, string[]>([['Sources', ['Jarvis']]]);
133
+
134
+ const result = createSelectionsModel(filters, selected);
135
+ expect(result).toEqual({
136
+ subOptions: {
137
+ Sources: {
138
+ values: ['Jarvis'],
139
+ },
140
+ },
141
+ });
142
+ });
143
+
144
+ test('top level, multiple filters, single value', () => {
145
+ const frontendModel = ModelsMocks.mockFrontendModelMixed();
146
+ const filters = frontendModel.options.subOptions ?? [];
147
+ const selected = new Map<string, string[]>([['Sources', ['KnowledgeBase', 'Jarvis']]]);
148
+
149
+ const result = createSelectionsModel(filters, selected);
150
+ expect(result).toEqual({
151
+ subOptions: {
152
+ Sources: {
153
+ values: ['KnowledgeBase', 'Jarvis'],
154
+ },
155
+ },
156
+ });
157
+ });
158
+
159
+ test('all levels, multiple filters, multiple values', () => {
160
+ const frontendModel = ModelsMocks.mockFrontendModelMixed();
161
+ const filters = frontendModel.options.subOptions ?? [];
162
+ const selected = new Map<string, string[]>([
163
+ ['Sources', ['KnowledgeBase', 'Jarvis']],
164
+ ['ContentTypes', ['kbReleaseNotes']],
165
+ ['ProductAreas', ['Marketing']],
166
+ ]);
167
+
168
+ const result = createSelectionsModel(filters, selected);
169
+ expect(result).toEqual({
170
+ subOptions: {
171
+ Sources: {
172
+ values: ['KnowledgeBase', 'Jarvis'],
173
+ subOptions: {
174
+ KnowledgeBase: {
175
+ subOptions: {
176
+ ContentTypes: {
177
+ values: ['kbReleaseNotes'],
178
+ },
179
+ ProductAreas: {
180
+ values: ['Marketing'],
181
+ },
182
+ },
183
+ },
184
+ },
185
+ },
186
+ },
187
+ });
188
+ });
189
+ });
190
+
191
+ describe('with nested filter structure', () => {
192
+ test('top level, single filter, single value', () => {
193
+ const frontendModel = ModelsMocks.mockFrontendModel();
194
+ const filters = frontendModel.options.subOptions ?? [];
195
+ const selected = new Map<string, string[]>([['Sources', ['KnowledgeBase']]]);
196
+
197
+ const result = createSelectionsModel(filters, selected);
198
+ expect(result).toEqual({
199
+ subOptions: {
200
+ Sources: {
201
+ values: ['KnowledgeBase'],
202
+ },
203
+ },
204
+ });
205
+ });
206
+
207
+ test('all levels, multiple filters, single value', () => {
208
+ const frontendModel = ModelsMocks.mockFrontendModel();
209
+ const filters = frontendModel.options.subOptions ?? [];
210
+ const selected = new Map<string, string[]>([
211
+ ['Sources', ['KnowledgeBase']],
212
+ ['ContentTypes', ['kbReleaseNotes']],
213
+ ]);
214
+
215
+ const result = createSelectionsModel(filters, selected);
216
+ expect(result).toEqual({
217
+ subOptions: {
218
+ Sources: {
219
+ values: ['KnowledgeBase'],
220
+ subOptions: {
221
+ KnowledgeBase: {
222
+ subOptions: {
223
+ ContentTypes: {
224
+ values: ['kbReleaseNotes'],
225
+ },
226
+ },
227
+ },
228
+ },
229
+ },
230
+ },
231
+ });
232
+ });
233
+
234
+ test('all levels, multiple filters, multiple values (with shared sub option)', () => {
235
+ const frontendModel = ModelsMocks.mockFrontendModel();
236
+ const filters = frontendModel.options.subOptions ?? [];
237
+ const selected = new Map<string, string[]>([
238
+ ['Sources', ['KnowledgeBase', 'Jarvis']],
239
+ ['ContentTypes', ['kbReleaseNotes']],
240
+ ]);
241
+
242
+ const result = createSelectionsModel(filters, selected);
243
+ expect(result).toEqual({
244
+ subOptions: {
245
+ Sources: {
246
+ values: ['KnowledgeBase', 'Jarvis'],
247
+ subOptions: {
248
+ KnowledgeBase: {
249
+ subOptions: {
250
+ ContentTypes: {
251
+ values: ['kbReleaseNotes'],
252
+ },
253
+ },
254
+ },
255
+ Jarvis: {
256
+ subOptions: {
257
+ ContentTypes: {
258
+ values: ['kbReleaseNotes'],
259
+ },
260
+ },
261
+ },
262
+ },
263
+ },
264
+ },
265
+ });
266
+ });
267
+
268
+ test('all levels, multiple filters, multiple values (with mixed sub options)', () => {
269
+ const frontendModel = ModelsMocks.mockFrontendModel();
270
+ const filters = frontendModel.options.subOptions ?? [];
271
+ const selected = new Map<string, string[]>([
272
+ ['Sources', ['KnowledgeBase', 'Jarvis']],
273
+ ['ContentTypes', ['kbReleaseNotes', 'kbFaq', 'xxx']],
274
+ ]);
275
+
276
+ const result = createSelectionsModel(filters, selected);
277
+ expect(result).toEqual({
278
+ subOptions: {
279
+ Sources: {
280
+ values: ['KnowledgeBase', 'Jarvis'],
281
+ subOptions: {
282
+ KnowledgeBase: {
283
+ subOptions: {
284
+ ContentTypes: {
285
+ values: ['kbReleaseNotes', 'kbFaq'],
286
+ },
287
+ },
288
+ },
289
+ Jarvis: {
290
+ subOptions: {
291
+ ContentTypes: {
292
+ values: ['kbReleaseNotes', 'xxx'],
293
+ },
294
+ },
295
+ },
296
+ },
297
+ },
298
+ },
299
+ });
300
+ });
301
+
302
+ test('all levels, multiple filters, multiple values', () => {
303
+ const frontendModel = ModelsMocks.mockFrontendModel();
304
+ const filters = frontendModel.options.subOptions ?? [];
305
+ const selected = new Map<string, string[]>([
306
+ ['Sources', ['KnowledgeBase']],
307
+ ['ProductAreas', ['Marketing', 'Marketing Pro']],
308
+ ]);
309
+
310
+ const result = createSelectionsModel(filters, selected);
311
+ expect(result).toEqual({
312
+ subOptions: {
313
+ Sources: {
314
+ values: ['KnowledgeBase'],
315
+ subOptions: {
316
+ KnowledgeBase: {
317
+ subOptions: {
318
+ ProductAreas: {
319
+ values: ['Marketing', 'Marketing Pro'],
320
+ },
321
+ },
322
+ },
323
+ },
324
+ },
325
+ },
326
+ });
327
+ });
328
+
329
+ test('all levels, multiple filters, multiple values (more values)', () => {
330
+ const frontendModel = ModelsMocks.mockFrontendModel();
331
+ const filters = frontendModel.options.subOptions ?? [];
332
+ const selected = new Map<string, string[]>([
333
+ ['Sources', ['KnowledgeBase']],
334
+ ['ContentTypes', ['kbReleaseNotes', 'kbHowTo']],
335
+ ['ProductAreas', ['Marketing']],
336
+ ]);
337
+
338
+ const result = createSelectionsModel(filters, selected);
339
+ expect(result).toEqual({
340
+ subOptions: {
341
+ Sources: {
342
+ values: ['KnowledgeBase'],
343
+ subOptions: {
344
+ KnowledgeBase: {
345
+ subOptions: {
346
+ ContentTypes: {
347
+ values: ['kbReleaseNotes', 'kbHowTo'],
348
+ },
349
+ ProductAreas: {
350
+ values: ['Marketing'],
351
+ },
352
+ },
353
+ },
354
+ },
355
+ },
356
+ },
357
+ });
358
+ });
359
+
360
+ test('all levels, multiple filters, multiple values (more values2)', () => {
361
+ const frontendModel = ModelsMocks.mockFrontendModel();
362
+ const filters = frontendModel.options.subOptions ?? [];
363
+ const selected = new Map<string, string[]>([
364
+ ['Sources', ['KnowledgeBase', 'Jarvis']],
365
+ ['ContentTypes', ['kbReleaseNotes', 'yyy', 'kbHowTo']],
366
+ ['ProductAreas', ['Marketing', 'Marketing Pro']],
367
+ ]);
368
+
369
+ const result = createSelectionsModel(filters, selected);
370
+ expect(result).toEqual({
371
+ subOptions: {
372
+ Sources: {
373
+ values: ['KnowledgeBase', 'Jarvis'],
374
+ subOptions: {
375
+ KnowledgeBase: {
376
+ subOptions: {
377
+ ContentTypes: {
378
+ values: ['kbReleaseNotes', 'kbHowTo'],
379
+ },
380
+ ProductAreas: {
381
+ values: ['Marketing', 'Marketing Pro'],
382
+ },
383
+ },
384
+ },
385
+ Jarvis: {
386
+ subOptions: {
387
+ ContentTypes: {
388
+ values: ['kbReleaseNotes', 'yyy'],
389
+ },
390
+ },
391
+ },
392
+ },
393
+ },
394
+ },
395
+ });
396
+ });
397
+ });
398
+
399
+ describe('edge cases', () => {
400
+ test('should ignore non-Group type filters', () => {
401
+ const customFilters: Models.IOption[] = [
402
+ new Models.Option({
403
+ key: 'NotAGroup',
404
+ displayName: 'Not A Group',
405
+ type: Models.OptionType.Selectable,
406
+ subOptions: [
407
+ new Models.Option({
408
+ key: 'option1',
409
+ displayName: 'Option 1',
410
+ type: Models.OptionType.Selectable,
411
+ }),
412
+ ],
413
+ }),
414
+ ];
415
+ const selected = new Map<string, string[]>([['NotAGroup', ['option1']]]);
416
+
417
+ const result = createSelectionsModel(customFilters, selected);
418
+
419
+ expect(result).toBeUndefined();
420
+ });
421
+
422
+ test('should handle filters with no subOptions', () => {
423
+ const customFilters: Models.IOption[] = [
424
+ new Models.Option({
425
+ key: 'EmptyFilter',
426
+ displayName: 'Empty Filter',
427
+ type: Models.OptionType.Group,
428
+ subOptions: [],
429
+ }),
430
+ ];
431
+ const selected = new Map<string, string[]>([['EmptyFilter', ['anything']]]);
432
+
433
+ const result = createSelectionsModel(customFilters, selected);
434
+
435
+ expect(result).toBeUndefined();
436
+ });
437
+
438
+ test('should create proper Selections model structure', () => {
439
+ const frontendModel = ModelsMocks.mockFrontendModel();
440
+ const filters = frontendModel.options.subOptions ?? [];
441
+ const selected = new Map<string, string[]>([
442
+ ['Sources', ['KnowledgeBase']],
443
+ ['ContentTypes', ['kbReleaseNotes']],
444
+ ]);
445
+
446
+ const result = createSelectionsModel(filters, selected);
447
+
448
+ expect(result).toBeInstanceOf(Models.Selections);
449
+ expect(result!.subOptions).toBeDefined();
450
+ expect(typeof result?.subOptions).toBe('object');
451
+ });
452
+ });
453
+ });
@@ -16,49 +16,68 @@ export function createSelectionsModel(
16
16
  subOptions: {},
17
17
  });
18
18
  }
19
+ return result.subOptions!;
19
20
  };
20
21
 
21
22
  const filterList = filters.filter(
22
23
  x => x.type === Models.OptionType.Group && Boolean(x.subOptions?.length)
23
24
  );
24
25
  for (const filter of filterList) {
25
- const hasSubFilters = Boolean(
26
- filter.subOptions?.some(x => Boolean(x.subOptions?.length))
26
+ const selectedLeafOptions = filter.subOptions!.filter(
27
+ x =>
28
+ x.type === Models.OptionType.Selectable &&
29
+ selected.get(filter.key)?.includes(x.key!)
27
30
  );
28
- if (!hasSubFilters) {
31
+ let values: string[] | undefined;
32
+ if (selectedLeafOptions.length) {
29
33
  // Leaf filter: just collect selected values
30
- const values = filter.subOptions
34
+ values = selectedLeafOptions
31
35
  ?.map(o => o.key)
32
36
  .filter(o => selected.get(filter.key)?.includes(o));
33
37
  if (values?.length) {
34
- ensureResult();
35
- result!.subOptions![filter.key] = new Models.Selections({ values });
38
+ const subOptions = ensureResult();
39
+ subOptions[filter.key] = new Models.Selections({ values });
36
40
  }
37
- } else {
38
- // Non-leaf filter: add selected options as selectables and process sub-filters
39
- const filterSelectables = filter.subOptions!.filter(
40
- x =>
41
- x.type === Models.OptionType.Selectable &&
42
- Boolean(x.subOptions?.length) &&
43
- selected.get(filter.key)?.includes(x.key!)
44
- );
45
- if (!filterSelectables.length) {
41
+ }
42
+ if (!selectedLeafOptions.length) {
43
+ continue;
44
+ }
45
+
46
+ // Non-leaf filter: add selected options as selectables and process sub-filters
47
+ const selectedNonLeafOptions = filter.subOptions!.filter(
48
+ x =>
49
+ x.type === Models.OptionType.Selectable &&
50
+ selected.get(filter.key)?.includes(x.key!) &&
51
+ Boolean(x.subOptions?.length)
52
+ );
53
+ if (!selectedNonLeafOptions.length) {
54
+ continue;
55
+ }
56
+
57
+ // We have some sub-filters to process: collect their results
58
+ const subFilterResults = new Map<string, Models.Selections>();
59
+ for (const nonLeafOption of selectedNonLeafOptions) {
60
+ const subFilters = nonLeafOption.subOptions!;
61
+ const resultSubFilters = process(subFilters);
62
+ if (!resultSubFilters) {
46
63
  continue;
47
64
  }
48
- ensureResult();
49
- const filterResult = new Models.Selections({
50
- subOptions: {},
51
- });
52
- result!.subOptions![filter.key] = filterResult;
53
- for (const filterSelectable of filterSelectables) {
54
- // Process sub-filters: if any sub-filters selected, add them to the result
55
- const subFilters = filterSelectable.subOptions!;
56
- const resultSubFilters = process(subFilters);
57
- filterResult.subOptions![filterSelectable.key] =
58
- resultSubFilters ??
59
- new Models.Selections({
60
- subOptions: {},
61
- });
65
+ subFilterResults.set(nonLeafOption.key!, resultSubFilters);
66
+ }
67
+
68
+ // If any sub-filters selected, ensure result and add them to the current subOptions
69
+ if (subFilterResults.size) {
70
+ const subOptions = ensureResult();
71
+ for (const [key, value] of subFilterResults) {
72
+ let subFilterSelection = subOptions[filter.key];
73
+ if (!subFilterSelection) {
74
+ subFilterSelection = new Models.Selections({});
75
+ subOptions[filter.key] = subFilterSelection;
76
+ }
77
+ if (!subFilterSelection.subOptions) {
78
+ subFilterSelection.subOptions = {};
79
+ }
80
+ subFilterSelection.subOptions![key] = value;
62
81
  }
63
82
  }
64
83
  }
@@ -277,6 +277,7 @@ describe('[FilterStore]', () => {
277
277
  expect(exported).toEqual({
278
278
  subOptions: {
279
279
  Sources: {
280
+ values: ['KnowledgeBase', 'Jarvis'],
280
281
  subOptions: {
281
282
  KnowledgeBase: {
282
283
  subOptions: {
@@ -314,11 +315,7 @@ describe('[FilterStore]', () => {
314
315
  expect(exported).toEqual({
315
316
  subOptions: {
316
317
  Sources: {
317
- subOptions: {
318
- KnowledgeBase: {
319
- subOptions: {},
320
- },
321
- },
318
+ values: ['KnowledgeBase'],
322
319
  },
323
320
  },
324
321
  });
@@ -335,6 +332,7 @@ describe('[FilterStore]', () => {
335
332
  expect(exported).toEqual({
336
333
  subOptions: {
337
334
  Sources: {
335
+ values: ['KnowledgeBase'],
338
336
  subOptions: {
339
337
  KnowledgeBase: {
340
338
  subOptions: {
@@ -359,6 +357,7 @@ describe('[FilterStore]', () => {
359
357
  expect(exported).toEqual({
360
358
  subOptions: {
361
359
  Sources: {
360
+ values: ['KnowledgeBase', 'Jarvis'],
362
361
  subOptions: {
363
362
  KnowledgeBase: {
364
363
  subOptions: {