@inseefr/lunatic 3.6.1 → 3.6.3

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 (63) hide show
  1. package/components/PairwiseLinks/PairwiseLinks.js +2 -0
  2. package/components/PairwiseLinks/PairwiseLinks.js.map +1 -1
  3. package/components/shared/Checkbox/CheckboxOption.d.ts +1 -0
  4. package/components/shared/Checkbox/CheckboxOption.js +2 -2
  5. package/components/shared/Checkbox/CheckboxOption.js.map +1 -1
  6. package/components/shared/Radio/RadioOption.js +2 -2
  7. package/components/shared/Radio/RadioOption.js.map +1 -1
  8. package/components/type.d.ts +4 -1
  9. package/esm/components/PairwiseLinks/PairwiseLinks.js +2 -0
  10. package/esm/components/PairwiseLinks/PairwiseLinks.js.map +1 -1
  11. package/esm/components/shared/Checkbox/CheckboxOption.d.ts +1 -0
  12. package/esm/components/shared/Checkbox/CheckboxOption.js +2 -2
  13. package/esm/components/shared/Checkbox/CheckboxOption.js.map +1 -1
  14. package/esm/components/shared/Radio/RadioOption.js +2 -2
  15. package/esm/components/shared/Radio/RadioOption.js.map +1 -1
  16. package/esm/components/type.d.ts +4 -1
  17. package/esm/type.source.d.ts +3 -0
  18. package/esm/use-lunatic/commons/variables/behaviours/cleaning-behaviour.d.ts +7 -2
  19. package/esm/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js +106 -33
  20. package/esm/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js.map +1 -1
  21. package/esm/use-lunatic/commons/variables/get-questionnaire-data.js +2 -1
  22. package/esm/use-lunatic/commons/variables/get-questionnaire-data.js.map +1 -1
  23. package/esm/use-lunatic/commons/variables/lunatic-variables-store.js +8 -1
  24. package/esm/use-lunatic/commons/variables/lunatic-variables-store.js.map +1 -1
  25. package/esm/use-lunatic/commons/variables/lunatic-variables-store.spec.js +76 -2
  26. package/esm/use-lunatic/commons/variables/lunatic-variables-store.spec.js.map +1 -1
  27. package/esm/use-lunatic/props/getComponentTypeProps.d.ts +4 -1
  28. package/esm/use-lunatic/props/propOptions.d.ts +6 -2
  29. package/esm/use-lunatic/props/propOptions.js +9 -7
  30. package/esm/use-lunatic/props/propOptions.js.map +1 -1
  31. package/esm/use-lunatic/props/propOptions.spec.js +68 -0
  32. package/esm/use-lunatic/props/propOptions.spec.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/components/PairwiseLinks/PairwiseLinks.tsx +1 -0
  35. package/src/components/shared/Checkbox/CheckboxOption.tsx +3 -0
  36. package/src/components/shared/Radio/RadioOption.tsx +2 -0
  37. package/src/components/type.ts +4 -1
  38. package/src/stories/pairwise/source.json +4 -1
  39. package/src/stories/questionnaires/simpsons/source.json +6 -0
  40. package/src/type.source.ts +3 -0
  41. package/src/use-lunatic/commons/variables/behaviours/cleaning-behaviour.ts +120 -30
  42. package/src/use-lunatic/commons/variables/get-questionnaire-data.ts +4 -1
  43. package/src/use-lunatic/commons/variables/lunatic-variables-store.spec.ts +87 -2
  44. package/src/use-lunatic/commons/variables/lunatic-variables-store.ts +9 -1
  45. package/src/use-lunatic/props/propOptions.spec.ts +88 -0
  46. package/src/use-lunatic/props/propOptions.ts +10 -5
  47. package/tsconfig.build.tsbuildinfo +1 -1
  48. package/type.source.d.ts +3 -0
  49. package/use-lunatic/commons/variables/behaviours/cleaning-behaviour.d.ts +7 -2
  50. package/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js +105 -32
  51. package/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js.map +1 -1
  52. package/use-lunatic/commons/variables/get-questionnaire-data.js +2 -1
  53. package/use-lunatic/commons/variables/get-questionnaire-data.js.map +1 -1
  54. package/use-lunatic/commons/variables/lunatic-variables-store.js +8 -1
  55. package/use-lunatic/commons/variables/lunatic-variables-store.js.map +1 -1
  56. package/use-lunatic/commons/variables/lunatic-variables-store.spec.js +76 -2
  57. package/use-lunatic/commons/variables/lunatic-variables-store.spec.js.map +1 -1
  58. package/use-lunatic/props/getComponentTypeProps.d.ts +4 -1
  59. package/use-lunatic/props/propOptions.d.ts +6 -2
  60. package/use-lunatic/props/propOptions.js +6 -4
  61. package/use-lunatic/props/propOptions.js.map +1 -1
  62. package/use-lunatic/props/propOptions.spec.js +68 -0
  63. package/use-lunatic/props/propOptions.spec.js.map +1 -1
@@ -18,6 +18,7 @@ export type CheckboxOptionProps = {
18
18
  invalid?: boolean;
19
19
  detailAlwaysDisplayed?: boolean;
20
20
  detailLabel?: ReactNode;
21
+ detailMaxLength?: number;
21
22
  detailValue?: string | null;
22
23
  onDetailChange?: (value: string) => void;
23
24
  };
@@ -32,6 +33,7 @@ function LunaticCheckboxOption({
32
33
  description,
33
34
  detailAlwaysDisplayed,
34
35
  detailLabel,
36
+ detailMaxLength,
35
37
  detailValue,
36
38
  onDetailChange,
37
39
  codeModality,
@@ -105,6 +107,7 @@ function LunaticCheckboxOption({
105
107
  id="detailId"
106
108
  label={detailLabel ?? 'Précisez :'}
107
109
  value={typeof detailValue === 'string' ? detailValue : ''}
110
+ maxLength={detailMaxLength}
108
111
  onChange={onDetailChange}
109
112
  disabled={disabled}
110
113
  />
@@ -38,6 +38,7 @@ function LunaticRadioOption({
38
38
  onDetailChange,
39
39
  detailAlwaysDisplayed,
40
40
  detailLabel,
41
+ detailMaxLength,
41
42
  detailValue,
42
43
  onCheck,
43
44
  onUncheck,
@@ -127,6 +128,7 @@ function LunaticRadioOption({
127
128
  id="detailId"
128
129
  label={detailLabel ?? 'Précisez :'}
129
130
  value={typeof detailValue === 'string' ? detailValue : ''}
131
+ maxLength={detailMaxLength}
130
132
  onChange={onDetailChange}
131
133
  disabled={disabled}
132
134
  />
@@ -195,8 +195,10 @@ export type ComponentPropsByType = {
195
195
  description?: ReactNode;
196
196
  onCheck: (b: boolean) => void;
197
197
  onDetailChange?: (v: string) => void;
198
- detailValue?: string | null;
199
198
  detailLabel?: ReactNode;
199
+ detailMaxLength?: number;
200
+ detailValue?: string | null;
201
+ shouldBeFiltered?: boolean;
200
202
  }[];
201
203
  orientation?: 'horizontal' | 'vertical';
202
204
  detailAlwaysDisplayed?: boolean;
@@ -251,6 +253,7 @@ export type ComponentPropsByType = {
251
253
  description?: ReactNode;
252
254
  label: ReactNode;
253
255
  value: string;
256
+ shouldBeFiltered?: boolean;
254
257
  }>;
255
258
  response: { name: string };
256
259
  componentType?: 'Dropdown';
@@ -90,7 +90,10 @@
90
90
  {
91
91
  "componentType": "Dropdown",
92
92
  "id": "dropdown-1",
93
- "conditionFilter": { "value": "xAxis <> yAxis", "type": "VTL" },
93
+ "conditionFilter": {
94
+ "value": "(nvl(xAxis, \"\") <> \"\") and (nvl(yAxis, \"\") <> \"\")",
95
+ "type": "VTL"
96
+ },
94
97
  "label": {
95
98
  "value": "\"Qui est \" || yAxis || \" pour \" || xAxis || \" ?\"",
96
99
  "type": "VTL|MD"
@@ -2,6 +2,12 @@
2
2
  "$schema": "../../../../lunatic-schema.json",
3
3
  "componentType": "Questionnaire",
4
4
  "variables": [
5
+ {
6
+ "variableType": "CALCULATED",
7
+ "expression": { "type": "VTL", "value": "\"2020\"" },
8
+ "name": "IGNOREDCALC",
9
+ "isIgnoredByLunatic": true
10
+ },
5
11
  {
6
12
  "variableType": "CALCULATED",
7
13
  "expression": { "type": "VTL", "value": "\"2020\"" },
@@ -145,6 +145,7 @@ export type ComponentCheckboxGroupDefinition = ComponentDefinitionBase & {
145
145
  id: string;
146
146
  detail?: {
147
147
  label?: VTLExpression;
148
+ maxLength?: number;
148
149
  response: ResponseDefinition;
149
150
  };
150
151
  }[];
@@ -164,6 +165,7 @@ export type OptionsWithDetail = {
164
165
  description?: VTLExpression;
165
166
  detail?: {
166
167
  label: VTLExpression;
168
+ maxLength?: number;
167
169
  response: {
168
170
  name: string;
169
171
  };
@@ -255,6 +257,7 @@ export type Variable =
255
257
  variableType: 'CALCULATED';
256
258
  name: string;
257
259
  expression: VTLExpression;
260
+ isIgnoredByLunatic?: boolean;
258
261
  bindingDependencies?: string[];
259
262
  shapeFrom?: string[] | string;
260
263
  iterationReference?: string;
@@ -3,17 +3,21 @@ import type {
3
3
  LunaticVariablesStore,
4
4
  } from '../lunatic-variables-store';
5
5
  import type { LunaticSource } from '../../../type';
6
- import { depth } from '../../../../utils/array';
6
+ import { depth, setAtIndex } from '../../../../utils/array';
7
7
  import { castBool } from '../../../../utils/cast';
8
8
 
9
9
  /**
10
- * Cleaning behaviour for the store
11
- * When a variable changes, other variables can be reset
10
+ * Implements the cleaning behavior for the variable store.
11
+ * When a variable changes, this function determines which other variables
12
+ * should be reset based on the cleaning rules defined in the source.
13
+ *
14
+ * @param store - The variables store that manages all variable values
15
+ * @param cleaning - Cleaning rules from the Lunatic source
16
+ * @param sourceValues - Default values from source.json to use when cleaning variables
12
17
  */
13
18
  export function cleaningBehaviour(
14
19
  store: LunaticVariablesStore,
15
20
  cleaning: LunaticSource['cleaning'],
16
- // Value used as default when cleaning a variable, correspoding to value of variable in source.json (not data)
17
21
  sourceValues: Record<string, unknown> = {}
18
22
  ) {
19
23
  if (!cleaning) {
@@ -37,31 +41,31 @@ export function cleaningBehaviour(
37
41
  }
38
42
  }
39
43
 
40
- // Create a map to improve performance
44
+ // Convert cleaning object to Map for faster lookups
41
45
  const cleaningMap = new Map(Object.entries(cleaning));
42
46
 
43
47
  store.on('change', (e) => {
44
48
  const cleaningInfo = cleaningMap.get(e.detail.name);
45
49
  const iteration = e.detail.iteration;
46
50
 
47
- // The variable does not have cleaning
51
+ // Skip if the changed variable doesn't have any cleaning rules
48
52
  if (!cleaningInfo) {
49
53
  return;
50
54
  }
51
55
 
52
56
  for (const variableName in cleaningInfo) {
53
57
  try {
54
- // First: check if variable is already cleaned i.e, value is `null`, empty or list of `null`
58
+ // Skip if variable is already in a cleaned state
55
59
  if (isAlreadyCleaned(store, variableName)) continue;
56
- // Second: check if variable should be clean i.e one of expressions is true
57
60
 
58
- // shouldClean is simple boolean or array of boolean if there have a shapeFrom
61
+ // Determine if the variable needs cleaning based on expressions
59
62
  const shouldCleanResult = shouldClean(store, {
60
63
  expressions: cleaningInfo[variableName],
61
64
  iteration: iteration,
62
65
  isResizing: e.detail.cause === 'resizing',
63
66
  });
64
67
 
68
+ // Handle array variables (where we might need to clean specific indexes)
65
69
  if (Array.isArray(shouldCleanResult)) {
66
70
  cleanArrayVariableAccordingCondition(
67
71
  store,
@@ -71,29 +75,51 @@ export function cleaningBehaviour(
71
75
  );
72
76
  continue;
73
77
  }
78
+
79
+ // Clean regular variables if needed
74
80
  if (shouldCleanResult)
75
81
  cleanVariable(store, sourceValues, variableName, iteration);
76
82
  } catch (e) {
77
- // If we have an error, skip this cleaning
83
+ // Log error but continue processing other variables
78
84
  console.error(e);
79
85
  }
80
86
  }
81
87
  });
82
88
  }
83
89
 
84
- function isAlreadyCleaned(store: LunaticVariablesStore, variableName: string) {
90
+ /**
91
+ * Checks if a variable is already in a cleaned state
92
+ *
93
+ * @param store - The variables store
94
+ * @param variableName - Name of the variable to check
95
+ * @returns true if the variable is null or an array of nulls
96
+ */
97
+ function isAlreadyCleaned(
98
+ store: LunaticVariablesStore,
99
+ variableName: string
100
+ ): boolean {
85
101
  const value = store.get(variableName);
86
102
  if (Array.isArray(value)) return value.every((v) => v === null);
87
- if (value === null) return true;
103
+ return value === null;
88
104
  }
89
105
 
90
106
  /**
91
- * Check if a variable need to be cleaned
107
+ * Determines if a variable needs to be cleaned based on expressions
108
+ *
109
+ * @param store - The variables store
110
+ * @param options - Configuration options
111
+ * @param options.expressions - Conditions that determine if cleaning should occur
112
+ * (string for legacy format or array of expression objects)
113
+ * /!\ Expressions comes from conditionFilter so it evaluates to "true"
114
+ * if the variable is visible, meaning we need to clean if the expression
115
+ * is evaluated to false.
116
+ * @param options.iteration - Current iteration level for the variable
117
+ * @param options.isResizing - Whether the cleaning is triggered by a resize operation
118
+ * @returns Boolean or array of booleans indicating if cleaning should occur
92
119
  */
93
120
  function shouldClean(
94
121
  store: LunaticVariablesStore,
95
122
  {
96
- // The expressions are a list of condition filter to display the variable, so we should clean if the filter is evaluated to false (false = variable is not visible)
97
123
  expressions,
98
124
  iteration,
99
125
  isResizing,
@@ -108,8 +134,8 @@ function shouldClean(
108
134
  iteration?: number[];
109
135
  isResizing: boolean;
110
136
  }
111
- ) {
112
- // Legacy cleaning used a simple string
137
+ ): boolean | boolean[] {
138
+ // Handle legacy format where expressions is a single string
113
139
  if (typeof expressions === 'string') {
114
140
  return !castBool(
115
141
  store.run(expressions, {
@@ -118,13 +144,13 @@ function shouldClean(
118
144
  );
119
145
  }
120
146
 
121
- // New format use tuples { expression, shapeFrom, isAggregatorUsed }
147
+ // Handle a new format with expression objects shaped like this { expression, shapeFrom, isAggregatorUsed }
122
148
  if (isResizing) {
123
- // If we are resizing a variable, only run expression containing aggregators (count(), sum()...)
149
+ // During resize operations, only consider expressions with aggregators (count, sum...)
124
150
  expressions = expressions.filter((expr) => expr.isAggregatorUsed);
125
151
  }
126
152
 
127
- // here, value has change in root scope, but we have to to check for each iteration of variable
153
+ // If expressions have shapeFrom and we're at root level, we need to check each iteration individually
128
154
  if (hasShapeFrom(expressions) && !iteration) {
129
155
  const shapeFromVariable = store.get(
130
156
  expressions[0].shapeFrom as string
@@ -143,9 +169,8 @@ function shouldClean(
143
169
  }
144
170
  return shouldCleanArray;
145
171
  } else {
146
- // if only one expression is false, we have to clean (condition is display condition)
172
+ // Clean the variable if any condition is false (variable is not visible),
147
173
  for (const expression of expressions) {
148
- // Run the expression to check if cleaning should happen
149
174
  if (
150
175
  !store.run(expression.expression, {
151
176
  iteration,
@@ -154,10 +179,16 @@ function shouldClean(
154
179
  return true;
155
180
  }
156
181
 
182
+ // All conditions are true, no cleaning needed
157
183
  return false;
158
184
  }
159
185
  }
160
186
 
187
+ /**
188
+ * Recursively retrieves a value at a specific iteration level in a nested structure
189
+ *
190
+ * @returns The value at the specified iteration path, or null if not found
191
+ */
161
192
  function getValueAtIteration(value: unknown, iteration?: number[]) {
162
193
  if (!iteration || iteration.length === 0) {
163
194
  return value ?? null;
@@ -169,12 +200,12 @@ function getValueAtIteration(value: unknown, iteration?: number[]) {
169
200
  }
170
201
 
171
202
  /**
172
- * hasShapeFrom
173
- * actually, in cleaning modelisation,
174
- * all expressions have no shapeFrom or have the **same** shapeFrom
175
- * @param expressions
176
- * @returns boolean if all expression has shapeFrom
203
+ * Checks if all expressions in the array have a shapeFrom property
204
+ *
205
+ * In the cleaning model, all expressions either have no shapeFrom
206
+ * or they all have the same shapeFrom value.
177
207
  *
208
+ * @returns true if all expressions have a non-null shapeFrom property
178
209
  */
179
210
  function hasShapeFrom(
180
211
  expressions: {
@@ -189,6 +220,14 @@ function hasShapeFrom(
189
220
  );
190
221
  }
191
222
 
223
+ /**
224
+ * Cleans specific elements in an array variable based on a condition array
225
+ *
226
+ * @param store - The variables store
227
+ * @param sourceValues - Default values from source.json
228
+ * @param variableName - Name of the array variable to clean
229
+ * @param shouldClean - Array of booleans indicating which elements should be cleaned
230
+ */
192
231
  function cleanArrayVariableAccordingCondition(
193
232
  store: LunaticVariablesStore,
194
233
  sourceValues: Record<string, unknown>,
@@ -202,10 +241,11 @@ function cleanArrayVariableAccordingCondition(
202
241
  }
203
242
 
204
243
  /**
205
- * cleanVariable: this function set to null (and not initalValue) the variable at iteration
206
- * @param store
207
- * @param variableName
208
- * @param iteration
244
+ * Resets a variable to its initial value at a specific iteration level
245
+ *
246
+ * This function retrieves the initial value from sourceValues and sets
247
+ * the variable to that value at the specified iteration. If the variable
248
+ * is a pairwise variable, it uses a special cleaning method to maintain symmetry.
209
249
  */
210
250
  function cleanVariable(
211
251
  store: LunaticVariablesStore,
@@ -220,6 +260,9 @@ function cleanVariable(
220
260
  ? undefined
221
261
  : iteration?.slice(0, depth(sourceValues[variableName]));
222
262
 
263
+ if (cleanPairwise(store, variableName, variableIteration)) {
264
+ return;
265
+ }
223
266
  store.set(
224
267
  variableName,
225
268
  getValueAtIteration(sourceValues[variableName], variableIteration),
@@ -229,3 +272,50 @@ function cleanVariable(
229
272
  }
230
273
  );
231
274
  }
275
+
276
+ /**
277
+ * Cleans a pairwise variable (2D array) at a specific iteration
278
+ *
279
+ * For pairwise variables, cleaning involves setting both the row and column
280
+ * at the specified index to null, maintaining the symmetry of the matrix.
281
+ *
282
+ * @returns true if the variable was a pairwise and was cleaned, false otherwise
283
+ */
284
+ function cleanPairwise(
285
+ store: LunaticVariablesStore,
286
+ variableName: string,
287
+ iteration?: IterationLevel
288
+ ): boolean {
289
+ // We are not trying to clean a pairwise at a specific index
290
+ if (!iteration || iteration.length !== 1) {
291
+ return false;
292
+ }
293
+ const variableValue = store.get(variableName);
294
+
295
+ // The variable is pairwise if it's a 2D array
296
+ if (!Array.isArray(variableValue)) {
297
+ return false;
298
+ }
299
+ const variableDepth = depth(variableValue);
300
+ if (variableDepth !== 2) {
301
+ return false;
302
+ }
303
+
304
+ // Clean the row and the column corresponding to the index
305
+ store.set(
306
+ variableName,
307
+ variableValue.map((value, k) => {
308
+ // The value is not an array, this should not happen so we keep the original value
309
+ if (!Array.isArray(value)) {
310
+ return value;
311
+ }
312
+ // Empty the row corresponding to the index being deleted
313
+ if (k === iteration[0]) {
314
+ return value.fill(null);
315
+ }
316
+ // Nullify cells in the column corresponding to the index being deleted
317
+ return setAtIndex(value, iteration, null);
318
+ })
319
+ );
320
+ return true;
321
+ }
@@ -40,7 +40,10 @@ export function getQuestionnaireData(
40
40
 
41
41
  for (const variable of variables) {
42
42
  // Skip calculated value if necessary
43
- if (variable.variableType === 'CALCULATED' && !withCalculated) {
43
+ if (
44
+ variable.variableType === 'CALCULATED' &&
45
+ (variable.isIgnoredByLunatic || !withCalculated)
46
+ ) {
44
47
  continue;
45
48
  }
46
49
 
@@ -268,7 +268,7 @@ describe('lunatic-variables-store', () => {
268
268
  variables.run('"hello"', { iteration: [0] });
269
269
  variables.run('"hello"', { iteration: [1] });
270
270
  expect(variables.run('"hello"')).toEqual('hello');
271
- expect(variables.interpretCount).toBe(3);
271
+ expect(variables.interpretCount).toBe(1); // only 1 interpretation by the engine since optimisation for primitive value
272
272
  });
273
273
  it('should handle deep refresh', () => {
274
274
  variables.set('LIENS', [
@@ -543,7 +543,7 @@ describe('lunatic-variables-store', () => {
543
543
  variables.set('READY', [true, true, false]);
544
544
  expect(variables.get('PRENOM')).toEqual(['John', 'Jane', null]);
545
545
  });
546
- it('should evaluate cleaning for each iteration at root levl', () => {
546
+ it('should evaluate cleaning for each iteration at root level', () => {
547
547
  variables.set('PRENOM', ['John', 'Jane', 'Marc']);
548
548
  variables.set('AGE', [18, 21, 23]);
549
549
  variables.setCalculated('NB_HAB', 'count(PRENOM)');
@@ -571,6 +571,45 @@ describe('lunatic-variables-store', () => {
571
571
  variables.set('PRENOM', ['John', 'Jane']);
572
572
  expect(variables.get('AGE')).toEqual([null, null]);
573
573
  });
574
+ it('should clean pairwise in two directions', () => {
575
+ variables.set('LINKS', [
576
+ [null, '1', '3'],
577
+ ['1', null, '3'],
578
+ ['2', '2', null],
579
+ ]);
580
+ variables.set('PRENOM', ['Dad', 'Mom', 'Unknow']);
581
+ cleaningBehaviour(
582
+ variables,
583
+ {
584
+ PRENOM: {
585
+ LINKS: [
586
+ {
587
+ expression: 'PRENOM <> ""',
588
+ shapeFrom: 'PRENOM',
589
+ isAggregatorUsed: true,
590
+ },
591
+ ],
592
+ },
593
+ },
594
+ {
595
+ PRENOM: [],
596
+ LINKS: [],
597
+ }
598
+ );
599
+ variables.set('PRENOM', '', { iteration: [1] });
600
+ expect(variables.get('PRENOM')).toEqual(['Dad', '', 'Unknow']);
601
+ expect(variables.get('LINKS')).toEqual([
602
+ [null, null, '3'],
603
+ [null, null, null],
604
+ ['2', null, null],
605
+ ]);
606
+ variables.set('PRENOM', '', { iteration: [0] });
607
+ expect(variables.get('LINKS')).toEqual([
608
+ [null, null, null],
609
+ [null, null, null],
610
+ [null, null, null],
611
+ ]);
612
+ });
574
613
  });
575
614
 
576
615
  describe('missing', () => {
@@ -657,6 +696,52 @@ describe('lunatic-variables-store', () => {
657
696
  store.commit();
658
697
  expect(store.get('PRENOM')).toEqual('John');
659
698
  });
699
+
700
+ it('should handle calculated variables, ignoring them when `isIgnoredByLunatic` is true ', () => {
701
+ const store = LunaticVariablesStore.makeFromSource(
702
+ {
703
+ components: [],
704
+ variables: [
705
+ {
706
+ name: 'calc1',
707
+ dimension: 0,
708
+ expression: {
709
+ type: 'VTL',
710
+ value: '"calculated value"',
711
+ },
712
+ variableType: 'CALCULATED',
713
+ isIgnoredByLunatic: true,
714
+ },
715
+ {
716
+ name: 'calc2',
717
+ dimension: 0,
718
+ expression: {
719
+ type: 'VTL',
720
+ value: '"calculated value"',
721
+ },
722
+ variableType: 'CALCULATED',
723
+ isIgnoredByLunatic: false,
724
+ },
725
+ {
726
+ name: 'calc3',
727
+ dimension: 0,
728
+ expression: {
729
+ type: 'VTL',
730
+ value: '"calculated value"',
731
+ },
732
+ variableType: 'CALCULATED',
733
+ },
734
+ ],
735
+ },
736
+ {},
737
+ { current: () => {} }
738
+ );
739
+
740
+ expect(store.get('calc1')).toBeNull();
741
+ expect(store.get('calc2')).toBe('calculated value');
742
+ expect(store.get('calc3')).toBe('calculated value');
743
+ });
744
+
660
745
  it('should enable cleaning when disableCleaning = false', () => {
661
746
  const cleaningSpy = vi.spyOn(cleaningModule, 'cleaningBehaviour');
662
747
  LunaticVariablesStore.makeFromSource(
@@ -88,6 +88,7 @@ export class LunaticVariablesStore {
88
88
  for (const variable of source.variables) {
89
89
  switch (variable.variableType) {
90
90
  case 'CALCULATED':
91
+ if (variable.isIgnoredByLunatic) break;
91
92
  store.setCalculated(variable.name, variable.expression.value, {
92
93
  dependencies: variable.bindingDependencies,
93
94
  iterationDepth: getIterationDepth(variable.name),
@@ -371,9 +372,16 @@ class LunaticVariable {
371
372
 
372
373
  // Calculate bindings first to refresh "updatedAt" on calculated dependencies
373
374
  const bindings = this.getDependenciesValues(iteration);
375
+ const hasNoBinding = Object.keys(bindings).length === 0;
376
+
377
+ // A static expression should not be reevaluated
378
+ if (hasNoBinding && this.value) {
379
+ return this.value;
380
+ }
381
+
374
382
  // A variable without binding is a primitive (string, boolean...)
375
383
  // it yields the same results for every iteration, so we can ignore iteration
376
- if (Object.keys(bindings).length === 0) {
384
+ if (hasNoBinding) {
377
385
  iteration = undefined;
378
386
  }
379
387
  if (this.shapeFrom && !this.isOutdated(iteration)) {
@@ -117,6 +117,94 @@ describe('getOptionsProp()', () => {
117
117
  { name: 'O2', value: false },
118
118
  ]);
119
119
  });
120
+ it('should create detail props correctly for checkboxGroup', () => {
121
+ const definition = {
122
+ ...checkboxGroupDefinition,
123
+ responses: [
124
+ {
125
+ label: 'Option 1',
126
+ response: { name: 'O1' },
127
+ id: 'id1',
128
+ detail: {
129
+ label: 'Precize:',
130
+ response: { name: 'DETAIL' },
131
+ maxLength: 50,
132
+ },
133
+ },
134
+ {
135
+ label: 'Option 2',
136
+ response: { name: 'O2' },
137
+ id: 'id2',
138
+ detail: {
139
+ label: 'Precize:',
140
+ response: { name: 'DETAILBIS' },
141
+ },
142
+ },
143
+ ],
144
+ } satisfies DeepTranslateExpression<LunaticComponentDefinition>;
145
+
146
+ variables.set('DETAIL', true);
147
+
148
+ const options = getOptionsProp(
149
+ definition,
150
+ variables,
151
+ mockChange,
152
+ undefined,
153
+ undefined,
154
+ mockLogger
155
+ );
156
+
157
+ expect(options).toHaveLength(2);
158
+ expect(options[0].detailLabel).toBe('Precize:');
159
+ expect(options[1].detailLabel).toBe('Precize:');
160
+ expect(options[0].detailValue).toBe(true);
161
+ expect(options[1].detailValue).toBeNull();
162
+ expect(options[0].detailMaxLength).toBe(50);
163
+ expect(options[1].detailMaxLength).toBeUndefined();
164
+ });
165
+ it('should create detail props correctly for Radiogroup', () => {
166
+ const definition = {
167
+ ...radioDefinition,
168
+ options: [
169
+ {
170
+ label: 'Option 1',
171
+ value: 'id1',
172
+ detail: {
173
+ label: 'Precize:',
174
+ response: { name: 'DETAIL' },
175
+ maxLength: 50,
176
+ },
177
+ },
178
+ {
179
+ label: 'Option 2',
180
+ value: 'id2',
181
+ detail: {
182
+ label: 'Precize:',
183
+ response: { name: 'DETAILBIS' },
184
+ },
185
+ },
186
+ ],
187
+ } satisfies DeepTranslateExpression<LunaticComponentDefinition>;
188
+
189
+ variables.set('DETAIL', true);
190
+
191
+ const options = getOptionsProp(
192
+ definition,
193
+ variables,
194
+ mockChange,
195
+ undefined,
196
+ undefined,
197
+ mockLogger
198
+ );
199
+
200
+ expect(options).toHaveLength(2);
201
+ expect(options[0].detailLabel).toBe('Precize:');
202
+ expect(options[1].detailLabel).toBe('Precize:');
203
+ expect(options[0].detailValue).toBe(true);
204
+ expect(options[1].detailValue).toBeNull();
205
+ expect(options[0].detailMaxLength).toBe(50);
206
+ expect(options[1].detailMaxLength).toBeUndefined();
207
+ });
120
208
  it('should filter responses (CheckboxGroup) with conditionFilter evaluated to false', () => {
121
209
  const definition = {
122
210
  ...checkboxGroupDefinition,