@inseefr/lunatic 3.8.0-rc.0 → 3.8.0-rc.ucq-options-variable.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 (33) hide show
  1. package/esm/type.source.d.ts +9 -3
  2. package/esm/use-lunatic/commons/fill-components/fill-component-expressions.d.ts +1 -1
  3. package/esm/use-lunatic/commons/fill-components/fill-component-expressions.js.map +1 -1
  4. package/esm/use-lunatic/commons/fill-components/fill-components.js +10 -2
  5. package/esm/use-lunatic/commons/fill-components/fill-components.js.map +1 -1
  6. package/esm/use-lunatic/props/propOptions.d.ts +9 -1
  7. package/esm/use-lunatic/props/propOptions.js +56 -1
  8. package/esm/use-lunatic/props/propOptions.js.map +1 -1
  9. package/esm/use-lunatic/props/propOptions.spec.js +220 -56
  10. package/esm/use-lunatic/props/propOptions.spec.js.map +1 -1
  11. package/package.json +4 -1
  12. package/src/stories/checkbox/checkbox.stories.tsx +13 -0
  13. package/src/stories/checkbox/sourceOneDynamicOptions.json +496 -0
  14. package/src/stories/dropdown/dropdown.stories.tsx +12 -0
  15. package/src/stories/dropdown/sourceDynamicOptions.json +496 -0
  16. package/src/stories/radio/radio.stories.tsx +13 -0
  17. package/src/stories/radio/sourceDynamicOptions.json +496 -0
  18. package/src/type.source.ts +9 -3
  19. package/src/use-lunatic/commons/fill-components/fill-component-expressions.ts +2 -1
  20. package/src/use-lunatic/commons/fill-components/fill-components.ts +9 -10
  21. package/src/use-lunatic/props/propOptions.spec.ts +217 -147
  22. package/src/use-lunatic/props/propOptions.ts +97 -8
  23. package/tsconfig.build.tsbuildinfo +1 -1
  24. package/type.source.d.ts +9 -3
  25. package/use-lunatic/commons/fill-components/fill-component-expressions.d.ts +1 -1
  26. package/use-lunatic/commons/fill-components/fill-component-expressions.js.map +1 -1
  27. package/use-lunatic/commons/fill-components/fill-components.js +9 -1
  28. package/use-lunatic/commons/fill-components/fill-components.js.map +1 -1
  29. package/use-lunatic/props/propOptions.d.ts +9 -1
  30. package/use-lunatic/props/propOptions.js +57 -2
  31. package/use-lunatic/props/propOptions.js.map +1 -1
  32. package/use-lunatic/props/propOptions.spec.js +217 -55
  33. package/use-lunatic/props/propOptions.spec.js.map +1 -1
@@ -1,46 +1,14 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import { LunaticVariablesStore } from '../commons/variables/lunatic-variables-store';
3
- import { getOptionsProp } from './propOptions';
3
+ import { computeOptionsFromComponent, InterpretedOption } from './propOptions';
4
4
  import type { DeepTranslateExpression } from '../commons/fill-components/fill-component-expressions';
5
5
  import type {
6
6
  LunaticChangesHandler,
7
7
  LunaticComponentDefinition,
8
8
  } from '../type';
9
9
 
10
- describe('getOptionsProp()', () => {
10
+ describe('computeOptionsFromComponent', () => {
11
11
  let variables: LunaticVariablesStore;
12
- const checkboxGroupDefinition = {
13
- id: 'CheckboxGroup',
14
- componentType: 'CheckboxGroup',
15
- responses: [
16
- {
17
- label: 'Option 1',
18
- response: { name: 'O1' },
19
- id: 'id1',
20
- },
21
- {
22
- label: 'Option 2',
23
- response: { name: 'O2' },
24
- id: 'id2',
25
- },
26
- ],
27
- } satisfies DeepTranslateExpression<LunaticComponentDefinition>;
28
-
29
- const radioDefinition = {
30
- id: 'RadioGroup',
31
- componentType: 'Radio',
32
- response: { name: 'RADIO' },
33
- options: [
34
- {
35
- label: 'Option 1',
36
- value: 'id1',
37
- },
38
- {
39
- label: 'Option 2',
40
- value: 'id2',
41
- },
42
- ],
43
- } satisfies DeepTranslateExpression<LunaticComponentDefinition>;
44
12
 
45
13
  let mockChange: LunaticChangesHandler;
46
14
  const mockLogger = vi.fn();
@@ -50,69 +18,97 @@ describe('getOptionsProp()', () => {
50
18
  variables = new LunaticVariablesStore();
51
19
  });
52
20
 
53
- describe('CheckboxGroup', () => {
21
+ describe('Options based on a fixed list', () => {
22
+ const checkboxGroupDefinition = {
23
+ id: 'CheckboxGroup',
24
+ componentType: 'CheckboxGroup',
25
+ responses: [
26
+ {
27
+ label: 'Option 1',
28
+ response: { name: 'O1' },
29
+ id: 'id1',
30
+ },
31
+ {
32
+ label: 'Option 2',
33
+ response: { name: 'O2' },
34
+ id: 'id2',
35
+ },
36
+ ],
37
+ } satisfies DeepTranslateExpression<LunaticComponentDefinition>;
38
+
39
+ const radioDefinition = {
40
+ id: 'RadioGroup',
41
+ componentType: 'Radio',
42
+ response: { name: 'RADIO' },
43
+ options: [
44
+ {
45
+ label: 'Option 1',
46
+ value: 'id1',
47
+ },
48
+ {
49
+ label: 'Option 2',
50
+ value: 'id2',
51
+ },
52
+ ],
53
+ } satisfies DeepTranslateExpression<LunaticComponentDefinition>;
54
+
54
55
  it('should check boxes', () => {
55
56
  variables.set('O2', false);
56
- let options = getOptionsProp(
57
- checkboxGroupDefinition,
57
+ let options = computeOptionsFromComponent(checkboxGroupDefinition, {
58
58
  variables,
59
- mockChange,
60
- undefined,
61
- undefined,
62
- mockLogger
63
- );
59
+ handleChanges: mockChange,
60
+ pagerIteration: undefined,
61
+ value: undefined,
62
+ logger: mockLogger,
63
+ });
64
64
  expect(options[1].checked).toBe(false);
65
65
  variables.set('O2', true);
66
- options = getOptionsProp(
67
- checkboxGroupDefinition,
66
+ options = computeOptionsFromComponent(checkboxGroupDefinition, {
68
67
  variables,
69
- mockChange,
70
- undefined,
71
- undefined,
72
- mockLogger
73
- );
68
+ handleChanges: mockChange,
69
+ pagerIteration: undefined,
70
+ value: undefined,
71
+ logger: mockLogger,
72
+ });
74
73
  expect(options[1].checked).toBe(true);
75
74
  });
76
75
  it('should check boxes correctly within iteration', () => {
77
76
  variables.set('O1', []);
78
77
  variables.set('O2', []);
79
- let options = getOptionsProp(
80
- checkboxGroupDefinition,
78
+ let options = computeOptionsFromComponent(checkboxGroupDefinition, {
81
79
  variables,
82
- mockChange,
83
- 0,
84
- undefined,
85
- mockLogger
86
- );
80
+ handleChanges: mockChange,
81
+ pagerIteration: 0,
82
+ value: undefined,
83
+ logger: mockLogger,
84
+ });
87
85
  expect(
88
86
  options.filter((o) => o.checked),
89
87
  'Nothing checked when variable empty'
90
88
  ).toHaveLength(0);
91
89
 
92
90
  variables.set('O1', [true, 0]);
93
- options = getOptionsProp(
94
- checkboxGroupDefinition,
91
+ options = computeOptionsFromComponent(checkboxGroupDefinition, {
95
92
  variables,
96
- mockChange,
97
- 0,
98
- undefined,
99
- mockLogger
100
- );
93
+ handleChanges: mockChange,
94
+ pagerIteration: 0,
95
+ value: undefined,
96
+ logger: mockLogger,
97
+ });
101
98
  expect(options[0].checked).toBe(true);
102
99
  expect(options[1].checked).toBe(false);
103
100
  });
104
101
  it('should create handleChange correctly', () => {
105
102
  variables.set('O1', [true, false]);
106
103
  variables.set('O2', [false, true]);
107
- const options = getOptionsProp(
108
- checkboxGroupDefinition,
104
+ const options = computeOptionsFromComponent(checkboxGroupDefinition, {
109
105
  variables,
110
- mockChange,
111
- 1,
112
- undefined,
113
- mockLogger
114
- );
115
- options[1].onCheck(false);
106
+ handleChanges: mockChange,
107
+ pagerIteration: 1,
108
+ value: undefined,
109
+ logger: mockLogger,
110
+ });
111
+ options[1].onCheck?.(false);
116
112
  expect(mockChange).toHaveBeenLastCalledWith([
117
113
  { name: 'O2', value: false },
118
114
  ]);
@@ -145,14 +141,13 @@ describe('getOptionsProp()', () => {
145
141
 
146
142
  variables.set('DETAIL', true);
147
143
 
148
- const options = getOptionsProp(
149
- definition,
144
+ const options = computeOptionsFromComponent(definition, {
150
145
  variables,
151
- mockChange,
152
- undefined,
153
- undefined,
154
- mockLogger
155
- );
146
+ handleChanges: mockChange,
147
+ pagerIteration: undefined,
148
+ value: undefined,
149
+ logger: mockLogger,
150
+ });
156
151
 
157
152
  expect(options).toHaveLength(2);
158
153
  expect(options[0].detailLabel).toBe('Precize:');
@@ -188,14 +183,13 @@ describe('getOptionsProp()', () => {
188
183
 
189
184
  variables.set('DETAIL', true);
190
185
 
191
- const options = getOptionsProp(
192
- definition,
186
+ const options = computeOptionsFromComponent(definition, {
193
187
  variables,
194
- mockChange,
195
- undefined,
196
- undefined,
197
- mockLogger
198
- );
188
+ handleChanges: mockChange,
189
+ pagerIteration: undefined,
190
+ value: undefined,
191
+ logger: mockLogger,
192
+ });
199
193
 
200
194
  expect(options).toHaveLength(2);
201
195
  expect(options[0].detailLabel).toBe('Precize:');
@@ -224,14 +218,13 @@ describe('getOptionsProp()', () => {
224
218
  ],
225
219
  } satisfies DeepTranslateExpression<LunaticComponentDefinition>;
226
220
 
227
- const options = getOptionsProp(
228
- definition,
221
+ const options = computeOptionsFromComponent(definition, {
229
222
  variables,
230
- mockChange,
231
- undefined,
232
- undefined,
233
- mockLogger
234
- );
223
+ handleChanges: mockChange,
224
+ pagerIteration: undefined,
225
+ value: undefined,
226
+ logger: mockLogger,
227
+ });
235
228
 
236
229
  // First option should be filtered out since its conditionFilter is evaluated to false
237
230
  expect(options).toHaveLength(1);
@@ -254,14 +247,13 @@ describe('getOptionsProp()', () => {
254
247
  ],
255
248
  } as any as DeepTranslateExpression<LunaticComponentDefinition>;
256
249
 
257
- const options = getOptionsProp(
258
- definition,
250
+ const options = computeOptionsFromComponent(definition, {
259
251
  variables,
260
- mockChange,
261
- undefined,
262
- undefined,
263
- mockLogger
264
- );
252
+ handleChanges: mockChange,
253
+ pagerIteration: undefined,
254
+ value: undefined,
255
+ logger: mockLogger,
256
+ });
265
257
 
266
258
  // First option should be filtered out since its conditionFilter is evaluated to false
267
259
  expect(options).toHaveLength(1);
@@ -285,14 +277,13 @@ describe('getOptionsProp()', () => {
285
277
  throw new Error('Test error');
286
278
  });
287
279
 
288
- const options = getOptionsProp(
289
- definition,
280
+ const options = computeOptionsFromComponent(definition, {
290
281
  variables,
291
- mockChange,
292
- undefined,
293
- undefined,
294
- mockLogger
295
- );
282
+ handleChanges: mockChange,
283
+ pagerIteration: undefined,
284
+ value: undefined,
285
+ logger: mockLogger,
286
+ });
296
287
 
297
288
  // Ensure the option is not filtered
298
289
  expect(options).toHaveLength(1);
@@ -315,14 +306,13 @@ describe('getOptionsProp()', () => {
315
306
  throw new Error('Test error');
316
307
  });
317
308
 
318
- const options = getOptionsProp(
319
- definition,
309
+ const options = computeOptionsFromComponent(definition, {
320
310
  variables,
321
- mockChange,
322
- undefined,
323
- undefined,
324
- mockLogger
325
- );
311
+ handleChanges: mockChange,
312
+ pagerIteration: undefined,
313
+ value: undefined,
314
+ logger: mockLogger,
315
+ });
326
316
 
327
317
  // Ensure the option is not filtered
328
318
  expect(options).toHaveLength(1);
@@ -346,15 +336,14 @@ describe('getOptionsProp()', () => {
346
336
  return false;
347
337
  });
348
338
 
349
- const options = getOptionsProp(
350
- definition,
339
+ const options = computeOptionsFromComponent(definition, {
351
340
  variables,
352
- mockChange,
353
- undefined,
354
- undefined,
355
- mockLogger,
356
- true // disableFilters = true
357
- );
341
+ handleChanges: mockChange,
342
+ pagerIteration: undefined,
343
+ value: undefined,
344
+ logger: mockLogger,
345
+ disableFilters: true,
346
+ });
358
347
 
359
348
  // Ensure the option is not filtered
360
349
  expect(options).toHaveLength(1);
@@ -378,15 +367,14 @@ describe('getOptionsProp()', () => {
378
367
  return false;
379
368
  });
380
369
 
381
- const options = getOptionsProp(
382
- definition,
370
+ const options = computeOptionsFromComponent(definition, {
383
371
  variables,
384
- mockChange,
385
- undefined,
386
- undefined,
387
- mockLogger,
388
- true // disableFilters = true
389
- );
372
+ handleChanges: mockChange,
373
+ pagerIteration: undefined,
374
+ value: undefined,
375
+ logger: mockLogger,
376
+ disableFilters: true,
377
+ });
390
378
 
391
379
  // Ensure the option is not filtered
392
380
  expect(options).toHaveLength(1);
@@ -406,16 +394,15 @@ describe('getOptionsProp()', () => {
406
394
  ],
407
395
  } satisfies DeepTranslateExpression<LunaticComponentDefinition>;
408
396
 
409
- const options = getOptionsProp(
410
- definition,
397
+ const options = computeOptionsFromComponent(definition, {
411
398
  variables,
412
- mockChange,
413
- undefined,
414
- undefined,
415
- mockLogger,
416
- true, // disableFilters = true
417
- true // parent component should be filtered
418
- );
399
+ handleChanges: mockChange,
400
+ pagerIteration: undefined,
401
+ value: undefined,
402
+ logger: mockLogger,
403
+ disableFilters: true,
404
+ shouldParentBeFiltered: true,
405
+ });
419
406
 
420
407
  // Ensure the option is not filtered
421
408
  expect(options).toHaveLength(1);
@@ -434,16 +421,15 @@ describe('getOptionsProp()', () => {
434
421
  ],
435
422
  } as any as DeepTranslateExpression<LunaticComponentDefinition>;
436
423
 
437
- const options = getOptionsProp(
438
- definition,
424
+ const options = computeOptionsFromComponent(definition, {
439
425
  variables,
440
- mockChange,
441
- undefined,
442
- undefined,
443
- mockLogger,
444
- true, // disableFilters = true
445
- true // parent component should be filtered
446
- );
426
+ handleChanges: mockChange,
427
+ pagerIteration: undefined,
428
+ value: undefined,
429
+ logger: mockLogger,
430
+ disableFilters: true,
431
+ shouldParentBeFiltered: true,
432
+ });
447
433
 
448
434
  // Ensure the option is not filtered
449
435
  expect(options).toHaveLength(1);
@@ -451,4 +437,88 @@ describe('getOptionsProp()', () => {
451
437
  expect(options[0].shouldBeFiltered).toBe(true);
452
438
  });
453
439
  });
440
+
441
+ describe('Options based on a source variable', () => {
442
+ const radioOptionSourceDefinition = {
443
+ id: 'RadioGroupDynamic',
444
+ componentType: 'Radio',
445
+ response: { name: 'RADIO' },
446
+ optionSource: 'NAME',
447
+ } satisfies DeepTranslateExpression<LunaticComponentDefinition>;
448
+
449
+ it('should build options when the source variable is an array of strings', () => {
450
+ variables.set('NAME', ['Maëlle', 'Verso']);
451
+ const options = computeOptionsFromComponent(radioOptionSourceDefinition, {
452
+ variables,
453
+ handleChanges: mockChange,
454
+ pagerIteration: undefined,
455
+ value: undefined,
456
+ logger: mockLogger,
457
+ }) as InterpretedOption[]; // force type but it should infer type correctly
458
+
459
+ expect(options).toHaveLength(2);
460
+ expect(options[0].value).toBe('Maëlle');
461
+ expect(options[0].label).toBe('Maëlle');
462
+ expect(options[1].value).toBe('Verso');
463
+ expect(options[1].label).toBe('Verso');
464
+ });
465
+
466
+ it('should build options when the source variable is an array of numbers', () => {
467
+ variables.set('NAME', [10, 20]);
468
+ const options = computeOptionsFromComponent(radioOptionSourceDefinition, {
469
+ variables,
470
+ handleChanges: mockChange,
471
+ pagerIteration: undefined,
472
+ value: undefined,
473
+ logger: mockLogger,
474
+ }) as InterpretedOption[]; // force type but it should infer type correctly
475
+
476
+ expect(options).toHaveLength(2);
477
+ expect(options[0].value).toBe(10);
478
+ expect(options[0].label).toBe('10');
479
+ expect(options[1].value).toBe(20);
480
+ expect(options[1].label).toBe('20');
481
+ });
482
+
483
+ it('should set the response when selecting a dynamic option', () => {
484
+ variables.set('NAME', ['Maëlle', 'Verso']);
485
+ const options = computeOptionsFromComponent(radioOptionSourceDefinition, {
486
+ variables,
487
+ handleChanges: mockChange,
488
+ pagerIteration: undefined,
489
+ value: undefined,
490
+ logger: mockLogger,
491
+ }) as InterpretedOption[]; // force type but it should infer type correctly
492
+
493
+ options[0].onCheck?.();
494
+ expect(mockChange).toHaveBeenLastCalledWith([
495
+ { name: 'RADIO', value: 'Maëlle' },
496
+ ]);
497
+
498
+ options[1].onCheck?.();
499
+ expect(mockChange).toHaveBeenLastCalledWith([
500
+ { name: 'RADIO', value: 'Verso' },
501
+ ]);
502
+ });
503
+
504
+ it('should filter options based on the optionFilter expression', () => {
505
+ const definition = {
506
+ ...radioOptionSourceDefinition,
507
+ optionFilter: { type: 'VTL', value: 'AGE >= 18' },
508
+ } satisfies DeepTranslateExpression<LunaticComponentDefinition>;
509
+
510
+ variables.set('NAME', ['Maëlle', 'Verso', 'Aline']);
511
+ variables.set('AGE', [16, 30, 50]);
512
+
513
+ const options = computeOptionsFromComponent(definition, {
514
+ variables,
515
+ handleChanges: mockChange,
516
+ pagerIteration: undefined,
517
+ value: undefined,
518
+ logger: mockLogger,
519
+ }) as InterpretedOption[]; // force type but it should infer type correctly
520
+
521
+ expect(options.map((option) => option.value)).toEqual(['Verso', 'Aline']);
522
+ });
523
+ });
454
524
  });
@@ -28,15 +28,25 @@ export type InterpretedOption = {
28
28
  /**
29
29
  * Compute options for checkboxes / radios / dropdown
30
30
  */
31
- export function getOptionsProp(
31
+ export function computeOptionsFromComponent(
32
32
  definition: DeepTranslateExpression<LunaticComponentDefinition>,
33
- variables: LunaticVariablesStore,
34
- handleChanges: LunaticChangesHandler,
35
- pagerIteration: LunaticState['pager']['iteration'],
36
- value: unknown,
37
- logger: LunaticLogger,
38
- disableFilters?: boolean,
39
- shouldParentBeFiltered?: boolean
33
+ {
34
+ variables,
35
+ handleChanges,
36
+ pagerIteration,
37
+ value,
38
+ logger,
39
+ disableFilters,
40
+ shouldParentBeFiltered,
41
+ }: {
42
+ variables: LunaticVariablesStore;
43
+ handleChanges: LunaticChangesHandler;
44
+ pagerIteration: LunaticState['pager']['iteration'];
45
+ value: unknown;
46
+ logger: LunaticLogger;
47
+ disableFilters?: boolean;
48
+ shouldParentBeFiltered?: boolean;
49
+ }
40
50
  ) {
41
51
  const iteration = isNumber(pagerIteration) ? [pagerIteration] : undefined;
42
52
 
@@ -85,10 +95,27 @@ export function getOptionsProp(
85
95
  }));
86
96
  }
87
97
 
98
+ // options based on another variable
99
+ if ('optionSource' in definition && definition.optionSource) {
100
+ return computeOptionsFromSource(definition.optionSource, {
101
+ variables,
102
+ value,
103
+ handleChanges,
104
+ responseName: definition.response.name,
105
+ logger,
106
+ shouldParentBeFiltered,
107
+ optionFilter: definition.optionFilter,
108
+ });
109
+ }
110
+
88
111
  if (!('options' in definition)) {
89
112
  return [];
90
113
  }
91
114
 
115
+ if (!definition.options) {
116
+ return [];
117
+ }
118
+
92
119
  return definition.options
93
120
  .filter((option) => {
94
121
  if (
@@ -144,6 +171,68 @@ export function getOptionsProp(
144
171
  }));
145
172
  }
146
173
 
174
+ /**
175
+ * Get all options from a source variable, applying filters.
176
+ */
177
+ function computeOptionsFromSource(
178
+ optionSource: string,
179
+ {
180
+ variables,
181
+ value,
182
+ handleChanges,
183
+ responseName,
184
+ logger,
185
+ shouldParentBeFiltered,
186
+ optionFilter,
187
+ }: {
188
+ variables: LunaticVariablesStore;
189
+ value: unknown;
190
+ handleChanges: LunaticChangesHandler;
191
+ responseName: string;
192
+ logger: LunaticLogger;
193
+ shouldParentBeFiltered?: boolean;
194
+ optionFilter?: VtlExpression;
195
+ }
196
+ ): InterpretedOption[] {
197
+ // we don't know the type of the optionSource values (string, numbers, boolean)
198
+ const optionValues = variables.get<unknown>(optionSource);
199
+ if (!optionValues) {
200
+ return [];
201
+ }
202
+
203
+ const normalizedValues = Array.isArray(optionValues)
204
+ ? optionValues
205
+ : [optionValues];
206
+
207
+ return normalizedValues
208
+ .filter((option, index) => {
209
+ // option is an empty value, we remove it from the options list
210
+ if (option === null || option === undefined) {
211
+ return false;
212
+ }
213
+ // no filter expression, we keep the option
214
+ if (!optionFilter) {
215
+ return true;
216
+ }
217
+ // apply filter expression on option (applied to its iteration)
218
+ return !isFilteredOutOption(variables, [index], logger, optionFilter);
219
+ })
220
+ .map((option) => {
221
+ return {
222
+ label: String(option),
223
+ value: option,
224
+ checked: value === option,
225
+ onCheck: () => {
226
+ handleChanges([{ name: responseName, value: option }]);
227
+ },
228
+ onUncheck: () => {
229
+ handleChanges([{ name: responseName, value: null }]);
230
+ },
231
+ shouldBeFiltered: shouldParentBeFiltered,
232
+ };
233
+ });
234
+ }
235
+
147
236
  /**
148
237
  * Check if an option should be filtered, depending on its conditionFilter.
149
238
  */