@inseefr/lunatic 3.4.21 → 3.5.0-rc.1

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 (36) hide show
  1. package/esm/type.source.d.ts +5 -1
  2. package/esm/use-lunatic/commons/variables/behaviours/cleaning-behaviour.d.ts +1 -1
  3. package/esm/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js +111 -14
  4. package/esm/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js.map +1 -1
  5. package/esm/use-lunatic/commons/variables/lunatic-variables-store.d.ts +1 -1
  6. package/esm/use-lunatic/commons/variables/lunatic-variables-store.spec.js +93 -0
  7. package/esm/use-lunatic/commons/variables/lunatic-variables-store.spec.js.map +1 -1
  8. package/esm/use-lunatic/use-lunatic.d.ts +1 -2
  9. package/esm/use-lunatic/use-lunatic.js.map +1 -1
  10. package/esm/utils/cast.d.ts +19 -0
  11. package/esm/utils/cast.js +63 -0
  12. package/esm/utils/cast.js.map +1 -0
  13. package/package.json +10 -1
  14. package/src/stories/behaviour/cleaning/cleaning.stories.jsx +11 -0
  15. package/src/stories/behaviour/cleaning/loop.json +246 -0
  16. package/src/stories/behaviour/performance/performance.stories.jsx +8 -0
  17. package/src/stories/behaviour/performance/srcv.json +44747 -0
  18. package/src/type.source.ts +7 -1
  19. package/src/use-lunatic/commons/variables/behaviours/cleaning-behaviour.ts +182 -24
  20. package/src/use-lunatic/commons/variables/lunatic-variables-store.spec.ts +111 -0
  21. package/src/use-lunatic/commons/variables/lunatic-variables-store.ts +1 -1
  22. package/src/use-lunatic/use-lunatic.ts +5 -4
  23. package/src/utils/cast.ts +67 -0
  24. package/tsconfig.build.tsbuildinfo +1 -1
  25. package/type.source.d.ts +5 -1
  26. package/use-lunatic/commons/variables/behaviours/cleaning-behaviour.d.ts +1 -1
  27. package/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js +111 -14
  28. package/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js.map +1 -1
  29. package/use-lunatic/commons/variables/lunatic-variables-store.d.ts +1 -1
  30. package/use-lunatic/commons/variables/lunatic-variables-store.spec.js +93 -0
  31. package/use-lunatic/commons/variables/lunatic-variables-store.spec.js.map +1 -1
  32. package/use-lunatic/use-lunatic.d.ts +1 -2
  33. package/use-lunatic/use-lunatic.js.map +1 -1
  34. package/utils/cast.d.ts +19 -0
  35. package/utils/cast.js +70 -0
  36. package/utils/cast.js.map +1 -0
@@ -272,7 +272,13 @@ export type LunaticSource = {
272
272
  suggesters?: SuggesterDefinition[];
273
273
  cleaning?: {
274
274
  [k: string]: {
275
- [k: string]: string;
275
+ [k: string]:
276
+ | string
277
+ | {
278
+ expression: string;
279
+ shapeFrom?: string;
280
+ isAggregatorUsed: boolean;
281
+ }[];
276
282
  };
277
283
  };
278
284
  missingBlock?: {
@@ -1,6 +1,10 @@
1
- import type { LunaticVariablesStore } from '../lunatic-variables-store';
1
+ import type {
2
+ IterationLevel,
3
+ LunaticVariablesStore,
4
+ } from '../lunatic-variables-store';
2
5
  import type { LunaticSource } from '../../../type';
3
6
  import { depth } from '../../../../utils/array';
7
+ import { castBool } from '../../../../utils/cast';
4
8
 
5
9
  /**
6
10
  * Cleaning behaviour for the store
@@ -9,13 +13,30 @@ import { depth } from '../../../../utils/array';
9
13
  export function cleaningBehaviour(
10
14
  store: LunaticVariablesStore,
11
15
  cleaning: LunaticSource['cleaning'],
12
- // Value used as default when cleaning a variable
13
- initialValues: Record<string, unknown> = {}
16
+ // Value used as default when cleaning a variable, correspoding to value of variable in source.json (not data)
17
+ sourceValues: Record<string, unknown> = {}
14
18
  ) {
15
19
  if (!cleaning) {
16
20
  return;
17
21
  }
18
22
 
23
+ // Create calculated variables from cleaning expressions
24
+ for (const source in cleaning) {
25
+ for (const target in cleaning[source]) {
26
+ if (Array.isArray(cleaning[source][target])) {
27
+ for (const cleaningInfo of cleaning[source][target]) {
28
+ store.setCalculated(
29
+ cleaningInfo.expression,
30
+ cleaningInfo.expression,
31
+ {
32
+ shapeFrom: cleaningInfo.shapeFrom,
33
+ }
34
+ );
35
+ }
36
+ }
37
+ }
38
+ }
39
+
19
40
  // Create a map to improve performance
20
41
  const cleaningMap = new Map(Object.entries(cleaning));
21
42
 
@@ -30,28 +51,28 @@ export function cleaningBehaviour(
30
51
 
31
52
  for (const variableName in cleaningInfo) {
32
53
  try {
33
- const skipCleaning = store.run(cleaningInfo[variableName], {
34
- iteration,
54
+ // First: check if variable is already cleaned i.e, value is `null`, empty or list of `null`
55
+ if (isAlreadyCleaned(store, variableName)) continue;
56
+ // Second: check if variable should be clean i.e one of expressions is true
57
+
58
+ // shouldClean is simple boolean or array of boolean if there have a shapeFrom
59
+ const shouldCleanResult = shouldClean(store, {
60
+ expressions: cleaningInfo[variableName],
61
+ iteration: iteration,
62
+ isResizing: e.detail.cause === 'resizing',
35
63
  });
36
- if (skipCleaning) {
64
+
65
+ if (Array.isArray(shouldCleanResult)) {
66
+ cleanArrayVariableAccordingCondition(
67
+ store,
68
+ sourceValues,
69
+ variableName,
70
+ shouldCleanResult
71
+ );
37
72
  continue;
38
73
  }
39
-
40
- // Variable may be top level, so we need to deduce expected iteration
41
- const variableDepth = depth(initialValues[variableName]);
42
- const variableIteration =
43
- variableDepth === 0
44
- ? undefined
45
- : iteration?.slice(0, depth(initialValues[variableName]));
46
-
47
- store.set(
48
- variableName,
49
- getValueAtIteration(initialValues[variableName], variableIteration),
50
- {
51
- iteration: variableIteration,
52
- cause: 'cleaning',
53
- }
54
- );
74
+ if (shouldCleanResult)
75
+ cleanVariable(store, sourceValues, variableName, iteration);
55
76
  } catch (e) {
56
77
  // If we have an error, skip this cleaning
57
78
  console.error(e);
@@ -60,14 +81,151 @@ export function cleaningBehaviour(
60
81
  });
61
82
  }
62
83
 
84
+ function isAlreadyCleaned(store: LunaticVariablesStore, variableName: string) {
85
+ const value = store.get(variableName);
86
+ if (Array.isArray(value)) return value.every((v) => v === null);
87
+ if (value === null) return true;
88
+ }
89
+
90
+ /**
91
+ * Check if a variable need to be cleaned
92
+ */
93
+ function shouldClean(
94
+ store: LunaticVariablesStore,
95
+ {
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
+ expressions,
98
+ iteration,
99
+ isResizing,
100
+ }: {
101
+ expressions:
102
+ | string
103
+ | {
104
+ expression: string;
105
+ shapeFrom?: string;
106
+ isAggregatorUsed: boolean;
107
+ }[];
108
+ iteration?: number[];
109
+ isResizing: boolean;
110
+ }
111
+ ) {
112
+ // Legacy cleaning used a simple string
113
+ if (typeof expressions === 'string') {
114
+ return !castBool(
115
+ store.run(expressions, {
116
+ iteration,
117
+ })
118
+ );
119
+ }
120
+
121
+ // New format use tuples { expression, shapeFrom, isAggregatorUsed }
122
+ if (isResizing) {
123
+ // If we are resizing a variable, only run expression containing aggregators (count(), sum()...)
124
+ expressions = expressions.filter((expr) => expr.isAggregatorUsed);
125
+ }
126
+
127
+ // here, value has change in root scope, but we have to to check for each iteration of variable
128
+ if (hasShapeFrom(expressions) && !iteration) {
129
+ const shapeFromVariable = store.get(
130
+ expressions[0].shapeFrom as string
131
+ ) as Array<unknown>;
132
+
133
+ const shouldCleanArray = new Array(shapeFromVariable.length).fill(
134
+ false
135
+ ) as Array<boolean>;
136
+
137
+ for (const [iterationIndex] of shouldCleanArray.entries()) {
138
+ shouldCleanArray[iterationIndex] = shouldClean(store, {
139
+ expressions,
140
+ iteration: [iterationIndex],
141
+ isResizing,
142
+ }) as boolean;
143
+ }
144
+ return shouldCleanArray;
145
+ } else {
146
+ // if only one expression is false, we have to clean (condition is display condition)
147
+ for (const expression of expressions) {
148
+ // Run the expression to check if cleaning should happen
149
+ if (
150
+ !store.run(expression.expression, {
151
+ iteration,
152
+ })
153
+ )
154
+ return true;
155
+ }
156
+
157
+ return false;
158
+ }
159
+ }
160
+
63
161
  function getValueAtIteration(value: unknown, iteration?: number[]) {
64
162
  if (!iteration || iteration.length === 0) {
65
163
  return value ?? null;
66
164
  }
67
-
68
165
  if (!Array.isArray(value)) {
69
166
  return null;
70
167
  }
71
-
72
168
  return getValueAtIteration(value[iteration[0]], iteration.slice(1));
73
169
  }
170
+
171
+ /**
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
177
+ *
178
+ */
179
+ function hasShapeFrom(
180
+ expressions: {
181
+ expression: string;
182
+ shapeFrom?: string;
183
+ isAggregatorUsed: boolean;
184
+ }[]
185
+ ) {
186
+ return expressions.every(
187
+ (expression) =>
188
+ expression.shapeFrom !== null && expression.shapeFrom !== undefined
189
+ );
190
+ }
191
+
192
+ function cleanArrayVariableAccordingCondition(
193
+ store: LunaticVariablesStore,
194
+ sourceValues: Record<string, unknown>,
195
+ variableName: string,
196
+ shouldClean: boolean[]
197
+ ) {
198
+ for (const [iteration, shouldCleanByIteration] of shouldClean.entries()) {
199
+ if (shouldCleanByIteration)
200
+ cleanVariable(store, sourceValues, variableName, [iteration]);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * cleanVariable: this function set to null (and not initalValue) the variable at iteration
206
+ * @param store
207
+ * @param variableName
208
+ * @param iteration
209
+ */
210
+ function cleanVariable(
211
+ store: LunaticVariablesStore,
212
+ sourceValues: Record<string, unknown>,
213
+ variableName: string,
214
+ iteration: IterationLevel | undefined
215
+ ) {
216
+ // Variable may be top level, so we need to deduce expected iteration
217
+ const variableDepth = depth(sourceValues[variableName]);
218
+ const variableIteration =
219
+ variableDepth === 0
220
+ ? undefined
221
+ : iteration?.slice(0, depth(sourceValues[variableName]));
222
+
223
+ store.set(
224
+ variableName,
225
+ getValueAtIteration(sourceValues[variableName], variableIteration),
226
+ {
227
+ iteration: variableIteration,
228
+ cause: 'cleaning',
229
+ }
230
+ );
231
+ }
@@ -104,6 +104,17 @@ describe('lunatic-variables-store', () => {
104
104
  expect(() => variables.run('Hello world')).toThrowError();
105
105
  });
106
106
 
107
+ it('should handle calculated with aggregates', () => {
108
+ variables.set('AGES', [1, 2, 3]);
109
+ variables.setCalculated('NB_HAB', 'count(AGES)');
110
+ variables.setCalculated('AGES_PLUS_NBHAB', 'NB_HAB + AGES', {
111
+ shapeFrom: 'AGES',
112
+ });
113
+ expect(variables.get('NB_HAB')).toBe(3);
114
+ expect(variables.get('AGES_PLUS_NBHAB', [0])).toBe(4);
115
+ expect(variables.get('AGES_PLUS_NBHAB')).toEqual([4, 5, 6]);
116
+ });
117
+
107
118
  describe('event listener', () => {
108
119
  it('should trigger onChange', () => {
109
120
  variables.set('FIRSTNAME', 'John');
@@ -415,6 +426,106 @@ describe('lunatic-variables-store', () => {
415
426
  variables.set('READY', false, { iteration: [1] });
416
427
  expect(variables.get('PRENOM')).toEqual(null);
417
428
  });
429
+ it('should handle the new array format', () => {
430
+ variables.set('PRENOM', ['John', 'Jane', 'Marc']);
431
+ variables.set('READY', [true, true, true]);
432
+ cleaningBehaviour(
433
+ variables,
434
+ {
435
+ READY: {
436
+ PRENOM: [
437
+ {
438
+ expression: 'READY',
439
+ shapeFrom: 'READY',
440
+ isAggregatorUsed: false,
441
+ },
442
+ ],
443
+ },
444
+ },
445
+ { PRENOM: [null] }
446
+ );
447
+ variables.set('READY', false, { iteration: [1] });
448
+ expect(variables.get('PRENOM')).toEqual(['John', null, 'Marc']);
449
+ });
450
+ it('should handle cleaning on aggregations', () => {
451
+ variables.set('PRENOM', ['John', 'Jane', 'Marc']);
452
+ variables.set('READY', [true, true, true]);
453
+ variables.setCalculated('NB_HAB', 'count(PRENOM)');
454
+ cleaningBehaviour(
455
+ variables,
456
+ {
457
+ READY: {
458
+ PRENOM: [
459
+ {
460
+ expression: 'READY',
461
+ shapeFrom: 'READY',
462
+ isAggregatorUsed: false,
463
+ },
464
+ {
465
+ expression: 'NB_HAB > 1',
466
+ isAggregatorUsed: true,
467
+ },
468
+ ],
469
+ },
470
+ },
471
+ { PRENOM: [null] }
472
+ );
473
+ variables.set('READY', false, { iteration: [1], cause: 'resizing' });
474
+ expect(variables.get('PRENOM')).toEqual(['John', 'Jane', 'Marc']);
475
+ variables.set('PRENOM', ['John'], { cause: 'resizing' });
476
+ variables.set('READY', [true], { cause: 'resizing' });
477
+ expect(variables.get('PRENOM')).toEqual([null]);
478
+ });
479
+
480
+ it('should evaluate cleaning for each iteration', () => {
481
+ variables.set('PRENOM', ['John', 'Jane', 'Marc']);
482
+ variables.set('READY', [true, true, true]);
483
+ cleaningBehaviour(
484
+ variables,
485
+ {
486
+ READY: {
487
+ PRENOM: [
488
+ {
489
+ expression: 'READY',
490
+ shapeFrom: 'READY',
491
+ isAggregatorUsed: false,
492
+ },
493
+ ],
494
+ },
495
+ },
496
+ { PRENOM: [null] }
497
+ );
498
+ variables.set('READY', [true, true, false]);
499
+ expect(variables.get('PRENOM')).toEqual(['John', 'Jane', null]);
500
+ });
501
+ it('should evaluate cleaning for each iteration at root levl', () => {
502
+ variables.set('PRENOM', ['John', 'Jane', 'Marc']);
503
+ variables.set('AGE', [18, 21, 23]);
504
+ variables.setCalculated('NB_HAB', 'count(PRENOM)');
505
+ resizingBehaviour(variables, {
506
+ PRENOM: {
507
+ size: 'count(PRENOM)',
508
+ variables: ['AGE'],
509
+ },
510
+ });
511
+ cleaningBehaviour(
512
+ variables,
513
+ {
514
+ PRENOM: {
515
+ AGE: [
516
+ {
517
+ expression: 'NB_HAB >= 3',
518
+ shapeFrom: 'PRENOM',
519
+ isAggregatorUsed: true,
520
+ },
521
+ ],
522
+ },
523
+ },
524
+ { PRENOM: [], AGE: [] }
525
+ );
526
+ variables.set('PRENOM', ['John', 'Jane']);
527
+ expect(variables.get('AGE')).toEqual([null, null]);
528
+ });
418
529
  });
419
530
 
420
531
  describe('missing', () => {
@@ -24,7 +24,7 @@ let interpretCount = 0;
24
24
  /** Special variable that will take the current iteration value. */
25
25
  const iterationVariableName = 'GLOBAL_ITERATION_INDEX';
26
26
 
27
- type IterationLevel = number[];
27
+ export type IterationLevel = number[];
28
28
  export type EventArgs = {
29
29
  change: {
30
30
  /** Name of the changed variable. */
@@ -6,17 +6,18 @@ import {
6
6
  handleChangesAction,
7
7
  } from './actions';
8
8
  import { getPageTag, isFirstLastPage } from './commons';
9
+
10
+ import D from '../i18n';
11
+ import { COLLECTED } from '../utils/constants';
12
+ import { createLunaticProvider } from './lunatic-context';
9
13
  import type {
14
+ LunaticSource,
10
15
  LunaticChangesHandler,
11
16
  LunaticData,
12
17
  LunaticOptions,
13
18
  LunaticState,
14
19
  PageTag,
15
20
  } from './type';
16
- import D from '../i18n';
17
- import { COLLECTED } from '../utils/constants';
18
- import { createLunaticProvider } from './lunatic-context';
19
- import type { LunaticSource } from './type';
20
21
  import { compileControls as compileControlsLib } from './commons/compile-controls';
21
22
  import { useLoopVariables } from './hooks/use-loop-variables';
22
23
  import { getQuestionnaireData } from './commons/variables/get-questionnaire-data';
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Cast functions that force an unknown type to a specific type
3
+ */
4
+
5
+ /**
6
+ * Force a number (throw if it cannot be converted)
7
+ */
8
+ export function castNumber(v: unknown): number {
9
+ if (typeof v === 'number') {
10
+ return v;
11
+ }
12
+ if (typeof v === 'string') {
13
+ return parseInt(v, 10);
14
+ }
15
+ if (Array.isArray(v) && v.length > 0) {
16
+ return castNumber(v[0]);
17
+ }
18
+ throw new Error(`Cannot cast "${v}" to number`);
19
+ }
20
+
21
+ /**
22
+ * Force a number, with a default value if an unmanageable type is encountered
23
+ */
24
+ export const castNumberWithDefault =
25
+ (initial: number = 0) =>
26
+ (v: unknown): number => {
27
+ try {
28
+ return castNumber(v);
29
+ } catch {
30
+ return initial;
31
+ }
32
+ };
33
+
34
+ /**
35
+ * Force a bool
36
+ */
37
+ export function castBool(v: unknown): boolean {
38
+ if (typeof v === 'boolean') {
39
+ return v;
40
+ }
41
+ if (Array.isArray(v) && v.length > 0) {
42
+ return castBool(v[0]);
43
+ }
44
+ if (Array.isArray(v)) {
45
+ return false;
46
+ }
47
+ return Boolean(v);
48
+ }
49
+
50
+ /**
51
+ * Force a string
52
+ */
53
+ export function castString(v: unknown): string {
54
+ if (typeof v === 'string') {
55
+ return v;
56
+ }
57
+ if (typeof v === 'number') {
58
+ return v.toString();
59
+ }
60
+ if (Array.isArray(v)) {
61
+ return v.map(castString).join(', ');
62
+ }
63
+ if (!v) {
64
+ return '';
65
+ }
66
+ return v.toString();
67
+ }