@inseefr/lunatic 3.7.7 → 3.8.0-rc.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 (62) hide show
  1. package/esm/type.source.d.ts +9 -0
  2. package/esm/use-lunatic/commons/compile-controls.js +2 -16
  3. package/esm/use-lunatic/commons/compile-controls.js.map +1 -1
  4. package/esm/use-lunatic/commons/component.d.ts +33 -0
  5. package/esm/use-lunatic/commons/component.js +37 -0
  6. package/esm/use-lunatic/commons/component.js.map +1 -1
  7. package/esm/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js.map +1 -1
  8. package/esm/use-lunatic/commons/variables/global-variables.d.ts +5 -0
  9. package/esm/use-lunatic/commons/variables/global-variables.js +7 -0
  10. package/esm/use-lunatic/commons/variables/global-variables.js.map +1 -0
  11. package/esm/use-lunatic/commons/variables/lunatic-variables-store.d.ts +21 -9
  12. package/esm/use-lunatic/commons/variables/lunatic-variables-store.js +67 -49
  13. package/esm/use-lunatic/commons/variables/lunatic-variables-store.js.map +1 -1
  14. package/esm/use-lunatic/commons/variables/lunatic-variables-store.spec.js +137 -6
  15. package/esm/use-lunatic/commons/variables/lunatic-variables-store.spec.js.map +1 -1
  16. package/esm/use-lunatic/commons/variables/models.d.ts +1 -0
  17. package/esm/use-lunatic/commons/variables/models.js +2 -0
  18. package/esm/use-lunatic/commons/variables/models.js.map +1 -0
  19. package/esm/use-lunatic/commons/variables/pairwise-variables.d.ts +29 -0
  20. package/esm/use-lunatic/commons/variables/pairwise-variables.js +196 -0
  21. package/esm/use-lunatic/commons/variables/pairwise-variables.js.map +1 -0
  22. package/esm/use-lunatic/props/getComponentTypeProps.d.ts +1 -1
  23. package/esm/use-lunatic/reducer/reducerInitializer.js +5 -1
  24. package/esm/use-lunatic/reducer/reducerInitializer.js.map +1 -1
  25. package/package.json +23 -1
  26. package/src/stories/pairwise/pairwise.stories.tsx +7 -0
  27. package/src/stories/pairwise/sourceGlobalVariables.json +337 -0
  28. package/src/type.source.ts +9 -0
  29. package/src/use-lunatic/commons/compile-controls.ts +10 -42
  30. package/src/use-lunatic/commons/component.ts +79 -0
  31. package/src/use-lunatic/commons/variables/behaviours/cleaning-behaviour.ts +2 -4
  32. package/src/use-lunatic/commons/variables/global-variables.ts +9 -0
  33. package/src/use-lunatic/commons/variables/lunatic-variables-store.spec.ts +149 -6
  34. package/src/use-lunatic/commons/variables/lunatic-variables-store.ts +113 -50
  35. package/src/use-lunatic/commons/variables/models.ts +1 -0
  36. package/src/use-lunatic/commons/variables/pairwise-variables.ts +251 -0
  37. package/src/use-lunatic/reducer/reducerInitializer.tsx +5 -7
  38. package/tsconfig.build.tsbuildinfo +1 -1
  39. package/type.source.d.ts +9 -0
  40. package/use-lunatic/commons/compile-controls.js +4 -18
  41. package/use-lunatic/commons/compile-controls.js.map +1 -1
  42. package/use-lunatic/commons/component.d.ts +33 -0
  43. package/use-lunatic/commons/component.js +42 -0
  44. package/use-lunatic/commons/component.js.map +1 -1
  45. package/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js.map +1 -1
  46. package/use-lunatic/commons/variables/global-variables.d.ts +5 -0
  47. package/use-lunatic/commons/variables/global-variables.js +11 -0
  48. package/use-lunatic/commons/variables/global-variables.js.map +1 -0
  49. package/use-lunatic/commons/variables/lunatic-variables-store.d.ts +21 -9
  50. package/use-lunatic/commons/variables/lunatic-variables-store.js +72 -50
  51. package/use-lunatic/commons/variables/lunatic-variables-store.js.map +1 -1
  52. package/use-lunatic/commons/variables/lunatic-variables-store.spec.js +137 -6
  53. package/use-lunatic/commons/variables/lunatic-variables-store.spec.js.map +1 -1
  54. package/use-lunatic/commons/variables/models.d.ts +1 -0
  55. package/use-lunatic/commons/variables/models.js +3 -0
  56. package/use-lunatic/commons/variables/models.js.map +1 -0
  57. package/use-lunatic/commons/variables/pairwise-variables.d.ts +29 -0
  58. package/use-lunatic/commons/variables/pairwise-variables.js +199 -0
  59. package/use-lunatic/commons/variables/pairwise-variables.js.map +1 -0
  60. package/use-lunatic/props/getComponentTypeProps.d.ts +1 -1
  61. package/use-lunatic/reducer/reducerInitializer.js +5 -1
  62. package/use-lunatic/reducer/reducerInitializer.js.map +1 -1
@@ -4,6 +4,7 @@ import * as cleaningModule from './behaviours/cleaning-behaviour';
4
4
  import { missingBehaviour } from './behaviours/missing-behaviour';
5
5
  import { resizingBehaviour } from './behaviours/resizing-behaviour';
6
6
  import { LunaticVariablesStore } from './lunatic-variables-store';
7
+ import { ComponentDefinitionWithPage } from '../../../type.source';
7
8
 
8
9
  describe('lunatic-variables-store', () => {
9
10
  let variables: LunaticVariablesStore;
@@ -710,7 +711,9 @@ describe('lunatic-variables-store', () => {
710
711
  },
711
712
  },
712
713
  },
713
- { current: () => {} }
714
+ {
715
+ changeHandler: { current: () => {} },
716
+ }
714
717
  );
715
718
  expect(store.get('PRENOM')).toEqual('Jane');
716
719
  store.set('NOM', 'Doe');
@@ -755,7 +758,9 @@ describe('lunatic-variables-store', () => {
755
758
  ],
756
759
  },
757
760
  {},
758
- { current: () => {} }
761
+ {
762
+ changeHandler: { current: () => {} },
763
+ }
759
764
  );
760
765
 
761
766
  expect(store.get('calc1')).toBeNull();
@@ -797,8 +802,10 @@ describe('lunatic-variables-store', () => {
797
802
  },
798
803
  },
799
804
  },
800
- { current: () => {} },
801
- false // enable cleaning
805
+ {
806
+ changeHandler: { current: () => {} },
807
+ disableCleaning: false, // enable cleaning
808
+ }
802
809
  );
803
810
  expect(cleaningSpy).toHaveBeenCalled();
804
811
  });
@@ -836,10 +843,146 @@ describe('lunatic-variables-store', () => {
836
843
  },
837
844
  },
838
845
  },
839
- { current: () => {} },
840
- true // disable cleaning
846
+ {
847
+ changeHandler: { current: () => {} },
848
+ disableCleaning: true, // disable cleaning
849
+ }
841
850
  );
842
851
  expect(cleaningSpy).not.toHaveBeenCalled();
843
852
  });
853
+
854
+ it('should create global pairwise variables', () => {
855
+ // Given a source with a pairwise component
856
+ const pairwiseComponent = {
857
+ id: 'm8ob5u9l',
858
+ page: '3',
859
+ symLinks: {
860
+ LINKS: {
861
+ '1': '1',
862
+ '2': '3',
863
+ '3': '2',
864
+ },
865
+ },
866
+ components: [
867
+ {
868
+ id: 'm8ob5u9l-pairwise-dropdown',
869
+ label: {
870
+ type: 'VTL|MD',
871
+ value: '"Qui est " || yAxis || " pour " || xAxis || " ?"',
872
+ },
873
+ options: [
874
+ {
875
+ label: {
876
+ type: 'VTL',
877
+ value: '"Son conjoint, sa conjointe"',
878
+ },
879
+ value: '1',
880
+ },
881
+ {
882
+ label: { type: 'VTL', value: '"Sa mère, son père"' },
883
+ value: '2',
884
+ },
885
+ {
886
+ label: { type: 'VTL', value: '"Sa fille, son fils"' },
887
+ value: '3',
888
+ },
889
+ ],
890
+ response: { name: 'LINKS' },
891
+ isMandatory: false,
892
+ componentType: 'Dropdown',
893
+ conditionFilter: {
894
+ type: 'VTL',
895
+ value: '(nvl(xAxis, "") <> "") and (nvl(yAxis, "") <> "")',
896
+ },
897
+ },
898
+ ],
899
+ sourceVariables: {
900
+ name: 'PRENOM',
901
+ gender: 'SEXE',
902
+ },
903
+ componentType: 'PairwiseLinks',
904
+ xAxisIterations: { type: 'VTL', value: 'count(PRENOM)' },
905
+ yAxisIterations: { type: 'VTL', value: 'count(PRENOM)' },
906
+ } as ComponentDefinitionWithPage;
907
+
908
+ // When we create the store
909
+ const store = LunaticVariablesStore.makeFromSource(
910
+ {
911
+ components: [pairwiseComponent],
912
+ variables: [
913
+ {
914
+ name: 'PRENOM',
915
+ values: { COLLECTED: [] },
916
+ dimension: 1,
917
+ variableType: 'COLLECTED',
918
+ iterationReference: 'm8ob7c76',
919
+ },
920
+ {
921
+ name: 'SEXE',
922
+ values: { COLLECTED: [] },
923
+ dimension: 1,
924
+ variableType: 'COLLECTED',
925
+ iterationReference: 'm8ob7c76',
926
+ },
927
+ {
928
+ name: 'LINKS',
929
+ values: { COLLECTED: [[]] },
930
+ dimension: 2,
931
+ variableType: 'COLLECTED',
932
+ iterationReference: 'm8ob7c76',
933
+ },
934
+ ],
935
+ },
936
+ {},
937
+ { changeHandler: { current: () => {} } }
938
+ );
939
+
940
+ // Then pairwise global variables are initialized
941
+ expect(store.get('GLOBAL_PARENT1_PRENOM', [0])).toBeUndefined();
942
+ expect(store.get('GLOBAL_PARENT2_PRENOM', [0])).toBeUndefined();
943
+ expect(store.get('GLOBAL_PARENT1_SEXE', [0])).toBeUndefined();
944
+ expect(store.get('GLOBAL_PARENT2_SEXE', [0])).toBeUndefined();
945
+ expect(store.get('GLOBAL_CONJOINT_PRENOM', [0])).toBeUndefined();
946
+ expect(store.get('GLOBAL_ENFANTS_PRENOMS', [0])).toBeUndefined();
947
+
948
+ // When pairwise link is updated
949
+ store.set('PRENOM', [
950
+ 'Verso',
951
+ 'Renoir',
952
+ 'Aline',
953
+ 'Monoco',
954
+ 'Noco',
955
+ 'Alicia',
956
+ 'Sciel',
957
+ ]);
958
+ store.set('SEXE', ['1', '1', '2', '1', '1', '2', '2']);
959
+ store.set('LINKS', [
960
+ [null, '2', '2', '3', '3', null, '1'],
961
+ ['3', null, '1', null, null, '3', null],
962
+ ['3', '1', null, null, null, '3', null],
963
+ ['2', null, null, null, null, null, null],
964
+ ['2', null, null, null, null, null, null],
965
+ [null, '2', '2', null, null, null, null],
966
+ ['1', null, null, null, null, null, null],
967
+ ]);
968
+ store.commit();
969
+
970
+ // Then the variables are set at the proper value
971
+ expect(store.get('GLOBAL_PARENT1_PRENOM', [0])).toBe('Renoir');
972
+ expect(store.get('GLOBAL_PARENT2_PRENOM', [0])).toBe('Aline');
973
+ expect(store.get('GLOBAL_PARENT1_SEXE', [0])).toBe('1');
974
+ expect(store.get('GLOBAL_PARENT2_SEXE', [0])).toBe('2');
975
+ expect(store.get('GLOBAL_CONJOINT_PRENOM', [0])).toBe('Sciel');
976
+ expect(store.get('GLOBAL_ENFANTS_PRENOMS', [0])).toBe('Monoco;Noco');
977
+
978
+ expect(store.get('GLOBAL_PARENT1_PRENOM', [1])).toBeUndefined();
979
+ expect(store.get('GLOBAL_PARENT2_PRENOM', [1])).toBeUndefined();
980
+ expect(store.get('GLOBAL_PARENT1_SEXE', [1])).toBeUndefined();
981
+ expect(store.get('GLOBAL_PARENT2_SEXE', [1])).toBeUndefined();
982
+ expect(store.get('GLOBAL_CONJOINT_PRENOM', [1])).toBe('Aline');
983
+ expect(store.get('GLOBAL_ENFANTS_PRENOMS', [1])).toBe('Verso;Alicia');
984
+
985
+ expect(store.get('GLOBAL_ENFANTS_PRENOMS', [4])).toBe(undefined);
986
+ });
844
987
  });
845
988
  });
@@ -18,13 +18,20 @@ import {
18
18
  VTLMissingDependencies,
19
19
  VTLMissingDependency,
20
20
  } from './errors';
21
+ import {
22
+ computePairwiseGlobalVariables,
23
+ computePairwiseGlobalVariableValue,
24
+ PairwiseGlobalDependency,
25
+ } from './pairwise-variables';
26
+ import {
27
+ computeGlobalIterationIndexValue,
28
+ GLOBAL_ITERATION_INDEX,
29
+ } from './global-variables';
30
+ import { IterationLevel } from './models';
21
31
 
22
32
  /** Interpret counter. Used for testing purpose. */
23
33
  let interpretCount = 0;
24
- /** Special variable that will take the current iteration value. */
25
- const iterationVariableName = 'GLOBAL_ITERATION_INDEX';
26
34
 
27
- export type IterationLevel = number[];
28
35
  export type EventArgs = {
29
36
  change: {
30
37
  /** Name of the changed variable. */
@@ -73,12 +80,16 @@ export class LunaticVariablesStore {
73
80
  public static makeFromSource(
74
81
  source: LunaticSource,
75
82
  data: LunaticData,
76
- changeHandler?: RefObject<LunaticOptions['onVariableChange']>,
77
- // Disable cleaning
78
- disableCleaning?: boolean,
79
- // Do not delay resizing / cleaning
80
- autoCommit?: boolean
83
+ options: {
84
+ changeHandler?: RefObject<LunaticOptions['onVariableChange']>;
85
+ // Disable cleaning
86
+ disableCleaning?: boolean;
87
+ // Do not delay resizing / cleaning
88
+ autoCommit?: boolean;
89
+ } = {}
81
90
  ) {
91
+ const { changeHandler, disableCleaning, autoCommit } = options;
92
+
82
93
  const store = new LunaticVariablesStore();
83
94
  if (typeof window !== 'undefined') {
84
95
  (window as any).lunaticStore = store; // Allow access to the store from the console
@@ -89,6 +100,15 @@ export class LunaticVariablesStore {
89
100
  if (autoCommit) {
90
101
  store.autoCommit = autoCommit;
91
102
  }
103
+
104
+ // Setup pairwise global variables if there is a pairwise component
105
+ const pairwiseVariables = computePairwiseGlobalVariables(source);
106
+ for (const pairwiseVariable of pairwiseVariables) {
107
+ const { name, dependencies, globalDependencies, shapeFrom } =
108
+ pairwiseVariable;
109
+ store.setGlobal(name, { dependencies, globalDependencies, shapeFrom });
110
+ }
111
+
92
112
  // Source data (picked from "variables" in the source.json)s
93
113
  const sourceValues: Record<string, unknown> = {};
94
114
  // Starting data for the form (merged with data.json or injected data)
@@ -106,6 +126,9 @@ export class LunaticVariablesStore {
106
126
  for (const variable of source.variables) {
107
127
  switch (variable.variableType) {
108
128
  case 'CALCULATED':
129
+ // In some cases, we have calculated variables in the source that are
130
+ // not used in the questionnaire (those variables are executed out of
131
+ // Lunatic.), so there is no need to add them to the dictionary.
109
132
  if (variable.isIgnoredByLunatic) break;
110
133
  store.setCalculated(variable.name, variable.expression.value, {
111
134
  dependencies: variable.bindingDependencies,
@@ -257,6 +280,38 @@ export class LunaticVariablesStore {
257
280
  return variable;
258
281
  }
259
282
 
283
+ /**
284
+ * Register global variable
285
+ */
286
+ public setGlobal(
287
+ name: string,
288
+ {
289
+ dependencies,
290
+ globalDependencies,
291
+ shapeFrom,
292
+ }: {
293
+ dependencies?: string[];
294
+ globalDependencies?: Map<unknown, string>;
295
+ shapeFrom?: string | string[];
296
+ }
297
+ ): LunaticVariable {
298
+ if (this.dictionary.has(name)) {
299
+ return this.dictionary.get(name)!;
300
+ }
301
+ const variable = new LunaticVariable({
302
+ dictionary: this.dictionary,
303
+ shapeFrom,
304
+ dependencies,
305
+ name,
306
+ storeUpdatedAt: this.updatedAt,
307
+ isGlobal: true,
308
+ globalDependencies,
309
+ });
310
+ this.dictionary.set(name, variable);
311
+ this.updatedAt.touch();
312
+ return variable;
313
+ }
314
+
260
315
  /**
261
316
  * Run a VTL expression
262
317
  */
@@ -317,7 +372,7 @@ export class LunaticVariablesStore {
317
372
  }
318
373
  }
319
374
 
320
- class LunaticVariable {
375
+ export class LunaticVariable {
321
376
  /** Last time the value was updated (changed). */
322
377
  public updatedAt = new Map<undefined | string, number>();
323
378
  /** Last time the store was updated (changed). */
@@ -328,8 +383,6 @@ class LunaticVariable {
328
383
  private value: unknown;
329
384
  /** List of direct dependencies, ex: ['FULLNAME', 'FIRSTNAME', 'LASTNAME']. */
330
385
  private dependencies?: string[];
331
- /** List of deep dependencies, exploring the variables used in calculated variables, ex: ['FIRSTNAME', 'LASTNAME']. */
332
- private baseDependencies?: string[];
333
386
  /** Expression for calculated variable. */
334
387
  public readonly expression?: string;
335
388
  /** Dictionary holding all the available variables. */
@@ -338,6 +391,10 @@ class LunaticVariable {
338
391
  private readonly iterationDepth?: number;
339
392
  /** For calculated variable, shape is copied from another variable. */
340
393
  private readonly shapeFrom?: string[];
394
+ /** Whether this is a global variable with custom computation rules. */
395
+ private readonly isGlobal?: boolean;
396
+ /** Name of variables needed for global variables computation rules. */
397
+ private readonly globalDependencies?: Map<unknown, string>;
341
398
  /** Keep a record of variable name (optional, used for debug). */
342
399
  public readonly name?: string;
343
400
  /** Count the number of calculation. */
@@ -354,12 +411,22 @@ class LunaticVariable {
354
411
  name?: string;
355
412
  dimension?: number;
356
413
  storeUpdatedAt: Timekey;
414
+ isGlobal?: boolean;
415
+ globalDependencies?: Map<unknown, string>;
357
416
  }) {
358
417
  if (args.expression && !args.dictionary) {
359
418
  throw new Error(
360
- `A calculated variable needs a dictionary to retrieve his deps`
419
+ 'A calculated variable needs a dictionary to retrieve his deps'
361
420
  );
362
421
  }
422
+ if (args.isGlobal && args.dependencies && !args.dictionary) {
423
+ throw new Error(
424
+ 'A global variable with dependencies needs a dictionary to retrieve its deps'
425
+ );
426
+ }
427
+ if (args.isGlobal && !args.name) {
428
+ throw new Error('A global variable needs a name to fetch its logic');
429
+ }
363
430
  this.expression = args.expression;
364
431
  this.dictionary = args.dictionary;
365
432
  this.dependencies = args.dependencies;
@@ -369,11 +436,13 @@ class LunaticVariable {
369
436
  this.name = args.name ?? args.expression;
370
437
  this.dimension = args.dimension;
371
438
  this.storeUpdatedAt = args.storeUpdatedAt;
439
+ this.isGlobal = args.isGlobal || false;
440
+ this.globalDependencies = args.globalDependencies || undefined;
372
441
  }
373
442
 
374
443
  getValue(iteration?: IterationLevel): unknown {
375
- // The variable is not calculated
376
- if (!this.expression) {
444
+ // The variable is not calculated or a global variable
445
+ if (!this.expression && !this.isGlobal) {
377
446
  return this.getSavedValue(iteration);
378
447
  }
379
448
 
@@ -424,6 +493,7 @@ class LunaticVariable {
424
493
  if (isTestEnv()) {
425
494
  interpretCount++;
426
495
  }
496
+
427
497
  // Scale down iteration if its dimension > shapeFrom dimension
428
498
  const shapeDimension = arrayDimension(shapeFromValue);
429
499
  if (
@@ -433,15 +503,30 @@ class LunaticVariable {
433
503
  ) {
434
504
  iteration = iteration.slice(0, shapeDimension);
435
505
  }
436
- // Uncomment this if you want to track the number of calculation
437
- // this.calculatedCount++;
438
- // Remember the value
439
- try {
440
- this.setValue(interpretVTL(this.expression, bindings), {
441
- iteration: iteration,
442
- });
443
- } catch {
444
- throw new VTLInterpretationError(this.expression!, bindings);
506
+
507
+ if (this.isGlobal) {
508
+ // compute global variable thanks to specific rule
509
+ const value = computePairwiseGlobalVariableValue(
510
+ this.name!,
511
+ iteration!,
512
+ this.globalDependencies! as Map<PairwiseGlobalDependency, string>,
513
+ this.dictionary!
514
+ );
515
+ this.setValue(value, { iteration });
516
+ } else {
517
+ // compute thanks to VTL expression
518
+
519
+ // Uncomment this if you want to track the number of calculation
520
+ // this.calculatedCount++;
521
+
522
+ // Remember the value
523
+ try {
524
+ this.setValue(interpretVTL(this.expression!, bindings), {
525
+ iteration: iteration,
526
+ });
527
+ } catch {
528
+ throw new VTLInterpretationError(this.expression!, bindings);
529
+ }
445
530
  }
446
531
  this.updateTimestamps(iteration, 'calculatedAt');
447
532
  return this.getSavedValue(iteration);
@@ -525,31 +610,6 @@ class LunaticVariable {
525
610
  return current;
526
611
  }
527
612
 
528
- /**
529
- * Get a list of transitive dependencies (leaf of the dependency tree)
530
- */
531
- private getBaseDependencies(): string[] {
532
- // Find the dependencies of the dependencies
533
- const reducer = (acc: Set<string>, variableName: string) => {
534
- if (acc.has(variableName)) {
535
- return acc;
536
- }
537
- const deps = this.dictionary?.get(variableName)?.getDependencies();
538
- if (!deps || deps.length === 0) {
539
- acc.add(variableName);
540
- } else {
541
- deps?.reduce(reducer, acc);
542
- }
543
- return acc;
544
- };
545
- if (this.baseDependencies === undefined) {
546
- this.baseDependencies = [
547
- ...this.getDependencies().reduce(reducer, new Set<string>()),
548
- ];
549
- }
550
- return this.baseDependencies;
551
- }
552
-
553
613
  private getDependencies(): string[] {
554
614
  // Calculate dependencies from expression on the fly if necessary
555
615
  if (this.dependencies === undefined) {
@@ -564,9 +624,12 @@ class LunaticVariable {
564
624
  try {
565
625
  return Object.fromEntries(
566
626
  this.getDependencies().map((dep) => {
567
- if (dep === iterationVariableName && iteration) {
568
- return [dep, iteration[0] + 1];
627
+ // The variable is a global variable with no dependency that we can
628
+ // manually compute on the fly.
629
+ if (dep === GLOBAL_ITERATION_INDEX && iteration) {
630
+ return computeGlobalIterationIndexValue(iteration);
569
631
  }
632
+
570
633
  const dependencyIteration =
571
634
  isNumber(this.iterationDepth) && Array.isArray(iteration)
572
635
  ? [iteration[this.iterationDepth]]
@@ -0,0 +1 @@
1
+ export type IterationLevel = number[];