@inseefr/lunatic 3.12.2 → 3.13.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 (73) hide show
  1. package/components/Dropdown/Dropdown.js +1 -2
  2. package/components/Dropdown/Dropdown.js.map +1 -1
  3. package/components/Dropdown/Dropdown.spec.js +13 -5
  4. package/components/Dropdown/Dropdown.spec.js.map +1 -1
  5. package/components/Suggester/CustomSuggester.d.ts +6 -0
  6. package/components/Suggester/CustomSuggester.js +1 -1
  7. package/components/Suggester/CustomSuggester.js.map +1 -1
  8. package/components/shared/Combobox/Combobox.js +5 -6
  9. package/components/shared/Combobox/Combobox.js.map +1 -1
  10. package/components/shared/Combobox/ComboboxType.d.ts +1 -0
  11. package/components/shared/Combobox/Panel/ComboboxOption.d.ts +1 -0
  12. package/components/shared/Combobox/Panel/ComboboxOption.js +4 -4
  13. package/components/shared/Combobox/Panel/ComboboxOption.js.map +1 -1
  14. package/components/shared/Combobox/Panel/ComboboxOption.spec.js +18 -5
  15. package/components/shared/Combobox/Panel/ComboboxOption.spec.js.map +1 -1
  16. package/components/shared/Combobox/Panel/ComboboxPanel.d.ts +1 -1
  17. package/components/shared/Combobox/Panel/ComboboxPanel.js +2 -2
  18. package/components/shared/Combobox/Panel/ComboboxPanel.js.map +1 -1
  19. package/components/type.d.ts +6 -0
  20. package/esm/components/Dropdown/Dropdown.js +1 -2
  21. package/esm/components/Dropdown/Dropdown.js.map +1 -1
  22. package/esm/components/Dropdown/Dropdown.spec.js +14 -6
  23. package/esm/components/Dropdown/Dropdown.spec.js.map +1 -1
  24. package/esm/components/Suggester/CustomSuggester.d.ts +6 -0
  25. package/esm/components/Suggester/CustomSuggester.js +1 -1
  26. package/esm/components/Suggester/CustomSuggester.js.map +1 -1
  27. package/esm/components/shared/Combobox/Combobox.js +5 -6
  28. package/esm/components/shared/Combobox/Combobox.js.map +1 -1
  29. package/esm/components/shared/Combobox/ComboboxType.d.ts +1 -0
  30. package/esm/components/shared/Combobox/Panel/ComboboxOption.d.ts +1 -0
  31. package/esm/components/shared/Combobox/Panel/ComboboxOption.js +5 -5
  32. package/esm/components/shared/Combobox/Panel/ComboboxOption.js.map +1 -1
  33. package/esm/components/shared/Combobox/Panel/ComboboxOption.spec.js +18 -5
  34. package/esm/components/shared/Combobox/Panel/ComboboxOption.spec.js.map +1 -1
  35. package/esm/components/shared/Combobox/Panel/ComboboxPanel.d.ts +1 -1
  36. package/esm/components/shared/Combobox/Panel/ComboboxPanel.js +2 -2
  37. package/esm/components/shared/Combobox/Panel/ComboboxPanel.js.map +1 -1
  38. package/esm/components/type.d.ts +6 -0
  39. package/esm/type.source.d.ts +1 -0
  40. package/esm/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js +44 -11
  41. package/esm/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js.map +1 -1
  42. package/esm/use-lunatic/commons/variables/lunatic-variables-store.spec.js +47 -0
  43. package/esm/use-lunatic/commons/variables/lunatic-variables-store.spec.js.map +1 -1
  44. package/package.json +2 -9
  45. package/src/components/Dropdown/Dropdown.spec.tsx +22 -6
  46. package/src/components/Dropdown/Dropdown.tsx +1 -2
  47. package/src/components/Dropdown/__snapshots__/Dropdown.spec.tsx.snap +2 -2
  48. package/src/components/Suggester/CustomSuggester.tsx +7 -0
  49. package/src/components/shared/Combobox/Combobox.tsx +5 -4
  50. package/src/components/shared/Combobox/ComboboxType.ts +1 -0
  51. package/src/components/shared/Combobox/Panel/ComboboxOption.spec.tsx +27 -5
  52. package/src/components/shared/Combobox/Panel/ComboboxOption.tsx +10 -6
  53. package/src/components/shared/Combobox/Panel/ComboboxPanel.tsx +3 -1
  54. package/src/components/type.ts +6 -0
  55. package/src/stories/checkbox/sourceOneDynamicOptions.json +16 -1
  56. package/src/stories/dropdown/sourceDynamicOptions.json +16 -1
  57. package/src/stories/radio/sourceDynamicOptions.json +16 -1
  58. package/src/type.source.ts +1 -0
  59. package/src/use-lunatic/commons/variables/behaviours/cleaning-behaviour.ts +74 -21
  60. package/src/use-lunatic/commons/variables/lunatic-variables-store.spec.ts +59 -0
  61. package/tsconfig.build.tsbuildinfo +1 -1
  62. package/type.source.d.ts +1 -0
  63. package/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js +44 -11
  64. package/use-lunatic/commons/variables/behaviours/cleaning-behaviour.js.map +1 -1
  65. package/use-lunatic/commons/variables/lunatic-variables-store.spec.js +47 -0
  66. package/use-lunatic/commons/variables/lunatic-variables-store.spec.js.map +1 -1
  67. package/components/Dropdown/renderer/SimpleOptionRenderer.d.ts +0 -7
  68. package/components/Dropdown/renderer/SimpleOptionRenderer.js +0 -16
  69. package/components/Dropdown/renderer/SimpleOptionRenderer.js.map +0 -1
  70. package/esm/components/Dropdown/renderer/SimpleOptionRenderer.d.ts +0 -7
  71. package/esm/components/Dropdown/renderer/SimpleOptionRenderer.js +0 -10
  72. package/esm/components/Dropdown/renderer/SimpleOptionRenderer.js.map +0 -1
  73. package/src/components/Dropdown/renderer/SimpleOptionRenderer.tsx +0 -26
@@ -15,7 +15,13 @@ type Props = {
15
15
  onBlur: () => void;
16
16
  onFocus: () => void;
17
17
  value: [SuggesterOptionType] | [];
18
+ /**
19
+ * @deprecated use createCustomizableField with ComboboxLabelRenderer as name.
20
+ */
18
21
  labelRenderer: LunaticComponentProps<'Suggester'>['labelRenderer'];
22
+ /**
23
+ * @deprecated use createCustomizableField with ComboboxOptionRenderer as name.
24
+ */
19
25
  optionRenderer: LunaticComponentProps<'Suggester'>['optionRenderer'];
20
26
  disabled?: boolean;
21
27
  readOnly?: boolean;
@@ -74,6 +80,7 @@ export const CustomSuggester = slottableComponent<Props>(
74
80
  disabled={disabled}
75
81
  readOnly={readOnly}
76
82
  options={options.map((o) => ({ value: o.id, ...o }))}
83
+ shouldDisplayOptionsId={true}
77
84
  editable={true}
78
85
  onBlur={onBlur}
79
86
  onFocus={onFocus}
@@ -57,6 +57,7 @@ function LunaticComboBox({
57
57
  onSelect,
58
58
  value,
59
59
  options,
60
+ shouldDisplayOptionsId = true,
60
61
  messageError,
61
62
  search = EMPTY_SEARCH,
62
63
  getOptionValue = getDefaultOptionValue,
@@ -184,6 +185,7 @@ function LunaticComboBox({
184
185
  isLoading={isLoading}
185
186
  optionRenderer={optionRenderer}
186
187
  options={options}
188
+ shouldDisplayOptionsId={shouldDisplayOptionsId}
187
189
  focused={focused}
188
190
  selectedIndex={selectedIndex}
189
191
  expanded={expanded}
@@ -212,15 +214,14 @@ function getIndexFromOptions(
212
214
  if (!Array.isArray(options)) {
213
215
  return undefined;
214
216
  }
215
- return options.map(getOptionValue).findIndex((v) => v === value);
217
+ return options.map(getOptionValue).indexOf(value ?? '');
216
218
  }
217
219
 
218
220
  /**
219
221
  * Extract value from an option item
220
222
  */
221
- function getDefaultOptionValue(option: ComboboxOptionType = { value: '' }) {
222
- const { id, value } = option;
223
- return id || value;
223
+ function getDefaultOptionValue(option?: ComboboxOptionType) {
224
+ return option?.id || option?.value || '';
224
225
  }
225
226
 
226
227
  export const Combobox = slottableComponent('Combobox', LunaticComboBox);
@@ -44,6 +44,7 @@ export type ComboboxPanelProps = {
44
44
  search?: string;
45
45
  }>;
46
46
  options: Array<ComboboxOptionType>;
47
+ shouldDisplayOptionsId?: boolean;
47
48
  focused?: boolean;
48
49
  selectedIndex?: number | string | null;
49
50
  expanded?: boolean;
@@ -14,13 +14,35 @@ describe('DefaultOptionRenderer', () => {
14
14
  expect(labelElement).toBeNull();
15
15
  });
16
16
 
17
- it('renders with label', () => {
17
+ it('renders with label and displays id by default', () => {
18
18
  const option = { id: '1', value: 'Value', label: 'Label' };
19
19
  const { getByText } = render(<ComboboxOption option={option} />);
20
- const idElement = getByText(option.id);
21
- const labelElement = getByText(option.label);
22
- expect(idElement).toBeInTheDocument();
23
- expect(labelElement).toBeInTheDocument();
20
+
21
+ expect(getByText(option.id)).toBeInTheDocument();
22
+ expect(getByText('-')).toBeInTheDocument();
23
+ expect(getByText(option.label)).toBeInTheDocument();
24
+ });
25
+
26
+ it('renders with label and displays id when prop is true', () => {
27
+ const option = { id: '1', value: 'Value', label: 'Label' };
28
+ const { getByText } = render(
29
+ <ComboboxOption option={option} shouldDisplayOptionId={true} />
30
+ );
31
+
32
+ expect(getByText(option.id)).toBeInTheDocument();
33
+ expect(getByText('-')).toBeInTheDocument();
34
+ expect(getByText(option.label)).toBeInTheDocument();
35
+ });
36
+
37
+ it('renders with label and does not display id when prop is false', () => {
38
+ const option = { id: '1', value: 'Value', label: 'Label' };
39
+ const { getByText, queryByText } = render(
40
+ <ComboboxOption option={option} shouldDisplayOptionId={false} />
41
+ );
42
+
43
+ expect(queryByText(option.id)).not.toBeInTheDocument();
44
+ expect(queryByText('-')).not.toBeInTheDocument();
45
+ expect(getByText(option.label)).toBeInTheDocument();
24
46
  });
25
47
 
26
48
  it('renders with selected class', () => {
@@ -5,12 +5,13 @@ import D from '../../../../i18n';
5
5
 
6
6
  type Props = {
7
7
  option: ComboboxOptionType;
8
+ shouldDisplayOptionId?: boolean;
8
9
  selected?: boolean;
9
10
  };
10
11
 
11
12
  export const ComboboxOption = slottableComponent(
12
13
  'ComboboxOption',
13
- ({ option, selected }: Props) => {
14
+ ({ option, shouldDisplayOptionId = true, selected }: Props) => {
14
15
  const { id, value, label } = option;
15
16
 
16
17
  if (value === 'OTHER') {
@@ -22,19 +23,22 @@ export const ComboboxOption = slottableComponent(
22
23
  </div>
23
24
  );
24
25
  }
25
-
26
- if (label && typeof label === 'string' && label.length) {
26
+ if (label) {
27
27
  return (
28
28
  <div className={classnames('lunatic-combo-box-option', { selected })}>
29
- <span className="id">{id || value}</span>
30
- <span> - </span>
29
+ {shouldDisplayOptionId && (
30
+ <>
31
+ <span className="id">{id || value}</span>
32
+ <span> - </span>
33
+ </>
34
+ )}
31
35
  <span className="label">{label}</span>
32
36
  </div>
33
37
  );
34
38
  }
35
39
  return (
36
40
  <div className={classnames('lunatic-combo-box-option', { selected })}>
37
- <span className="id">{id}</span>
41
+ <span className="id">{id || value}</span>
38
42
  </div>
39
43
  );
40
44
  }
@@ -10,6 +10,7 @@ import D from '../../../../i18n';
10
10
  export function ComboboxPanel({
11
11
  optionRenderer: OptionRender,
12
12
  options = [],
13
+ shouldDisplayOptionsId,
13
14
  focused,
14
15
  selectedIndex,
15
16
  expanded,
@@ -17,7 +18,7 @@ export function ComboboxPanel({
17
18
  search,
18
19
  onSelect,
19
20
  isLoading,
20
- }: ComboboxPanelProps) {
21
+ }: Readonly<ComboboxPanelProps>) {
21
22
  const visibleOptions = expanded ? options : [];
22
23
 
23
24
  const ComboBoxOptionComponent = OptionRender ?? ComboboxOption;
@@ -84,6 +85,7 @@ export function ComboboxPanel({
84
85
  >
85
86
  <ComboBoxOptionComponent
86
87
  option={option}
88
+ shouldDisplayOptionId={shouldDisplayOptionsId}
87
89
  selected={selectedIndex === index}
88
90
  search={search}
89
91
  />
@@ -261,11 +261,17 @@ export type ComponentPropsByType = {
261
261
  Suggester: LunaticBaseProps<string | null> & {
262
262
  componentType?: 'Suggester';
263
263
  storeName: string;
264
+ /**
265
+ * @deprecated use createCustomizableField with ComboboxOptionRenderer as name.
266
+ */
264
267
  optionRenderer: FunctionComponent<{
265
268
  option: SuggesterOption;
266
269
  placeholder?: string;
267
270
  search?: string;
268
271
  }>;
272
+ /**
273
+ * @deprecated use createCustomizableField with ComboboxLabelRenderer as name.
274
+ */
269
275
  labelRenderer: FunctionComponent<{
270
276
  option?: SuggesterOption;
271
277
  selected?: boolean;
@@ -6,7 +6,22 @@
6
6
  },
7
7
  "modele": "FEATQCUBAS",
8
8
  "maxPage": "7",
9
- "cleaning": {},
9
+ "cleaning": {
10
+ "PRENOM": {
11
+ "QCUHORSBOU": [
12
+ {
13
+ "expression": false
14
+ }
15
+ ],
16
+ "QCUDANSBOU": [
17
+ {
18
+ "expression": false,
19
+ "shapeFrom": "PRENOM",
20
+ "shouldCheckAllIterations": true
21
+ }
22
+ ]
23
+ }
24
+ },
10
25
  "resizing": {
11
26
  "NBHAB": {
12
27
  "size": "NBHAB",
@@ -6,7 +6,22 @@
6
6
  },
7
7
  "modele": "FEATQCUBAS",
8
8
  "maxPage": "7",
9
- "cleaning": {},
9
+ "cleaning": {
10
+ "PRENOM": {
11
+ "QCUHORSBOU": [
12
+ {
13
+ "expression": false
14
+ }
15
+ ],
16
+ "QCUDANSBOU": [
17
+ {
18
+ "expression": false,
19
+ "shapeFrom": "PRENOM",
20
+ "shouldCheckAllIterations": true
21
+ }
22
+ ]
23
+ }
24
+ },
10
25
  "resizing": {
11
26
  "NBHAB": {
12
27
  "size": "NBHAB",
@@ -6,7 +6,22 @@
6
6
  },
7
7
  "modele": "FEATQCUBAS",
8
8
  "maxPage": "7",
9
- "cleaning": {},
9
+ "cleaning": {
10
+ "PRENOM": {
11
+ "QCUHORSBOU": [
12
+ {
13
+ "expression": false
14
+ }
15
+ ],
16
+ "QCUDANSBOU": [
17
+ {
18
+ "expression": false,
19
+ "shapeFrom": "PRENOM",
20
+ "shouldCheckAllIterations": true
21
+ }
22
+ ]
23
+ }
24
+ },
10
25
  "resizing": {
11
26
  "NBHAB": {
12
27
  "size": "NBHAB",
@@ -302,6 +302,7 @@ export type LunaticSource = {
302
302
  expression: string;
303
303
  shapeFrom?: string;
304
304
  isAggregatorUsed: boolean;
305
+ shouldCheckAllIterations?: boolean;
305
306
  }[];
306
307
  };
307
308
  };
@@ -7,6 +7,13 @@ import { depth, setAtIndex } from '../../../../utils/array';
7
7
  import { castBool } from '../../../../utils/cast';
8
8
  import { IterationLevel } from '../models';
9
9
 
10
+ type CleaningExpression = {
11
+ expression: string;
12
+ shapeFrom?: string;
13
+ isAggregatorUsed: boolean;
14
+ shouldCheckAllIterations?: boolean;
15
+ };
16
+
10
17
  /**
11
18
  * Implements the cleaning behavior for the variable store.
12
19
  * When a variable changes, this function determines which other variables
@@ -130,13 +137,7 @@ function shouldClean(
130
137
  iteration,
131
138
  isResizing,
132
139
  }: {
133
- expressions:
134
- | string
135
- | {
136
- expression: string;
137
- shapeFrom?: string;
138
- isAggregatorUsed: boolean;
139
- }[];
140
+ expressions: string | CleaningExpression[];
140
141
  iteration?: number[];
141
142
  isResizing: boolean;
142
143
  }
@@ -156,6 +157,42 @@ function shouldClean(
156
157
  expressions = expressions.filter((expr) => expr.isAggregatorUsed);
157
158
  }
158
159
 
160
+ // At least one expression requires to compute on each iteration
161
+ if (shouldCheckAtAllIterations(expressions)) {
162
+ const shapeFromVariable = store.get(
163
+ expressions[0].shapeFrom as string
164
+ ) as Array<unknown>;
165
+
166
+ const shouldCleanArray = new Array(shapeFromVariable.length).fill(
167
+ false
168
+ ) as Array<boolean>;
169
+
170
+ const expressionsToCheckAtAllIterations: CleaningExpression[] = [];
171
+ const expressionsNotToCheckAtAllIterations: CleaningExpression[] = [];
172
+
173
+ for (const expression of expressions) {
174
+ if (expression.shouldCheckAllIterations) {
175
+ expressionsToCheckAtAllIterations.push(expression);
176
+ } else {
177
+ expressionsNotToCheckAtAllIterations.push(expression);
178
+ }
179
+ }
180
+
181
+ for (const [iterationIndex] of shouldCleanArray.entries()) {
182
+ shouldCleanArray[iterationIndex] =
183
+ // check if we need to clean each iteration according to expression
184
+ shouldCleanAtIteration(store, {
185
+ expressions: expressionsToCheckAtAllIterations,
186
+ iteration: [iterationIndex],
187
+ }) ||
188
+ shouldCleanAtIteration(store, {
189
+ expressions: expressionsNotToCheckAtAllIterations,
190
+ iteration,
191
+ });
192
+ }
193
+ return shouldCleanArray;
194
+ }
195
+
159
196
  // If expressions have shapeFrom and we're at root level, we need to check each iteration individually
160
197
  if (hasShapeFrom(expressions) && !iteration) {
161
198
  const shapeFromVariable = store.get(
@@ -167,27 +204,43 @@ function shouldClean(
167
204
  ) as Array<boolean>;
168
205
 
169
206
  for (const [iterationIndex] of shouldCleanArray.entries()) {
170
- shouldCleanArray[iterationIndex] = shouldClean(store, {
207
+ shouldCleanArray[iterationIndex] = shouldCleanAtIteration(store, {
171
208
  expressions,
172
209
  iteration: [iterationIndex],
173
- isResizing,
174
- }) as boolean;
210
+ });
175
211
  }
176
212
  return shouldCleanArray;
177
213
  } else {
178
- // Clean the variable if any condition is false (variable is not visible),
179
- for (const expression of expressions) {
180
- if (
181
- !store.run(expression.expression, {
182
- iteration,
183
- })
184
- )
185
- return true;
186
- }
214
+ return shouldCleanAtIteration(store, { expressions, iteration });
215
+ }
216
+ }
187
217
 
188
- // All conditions are true, no cleaning needed
189
- return false;
218
+ function shouldCheckAtAllIterations(expressions: CleaningExpression[]) {
219
+ return expressions.some((expression) => expression.shouldCheckAllIterations);
220
+ }
221
+
222
+ function shouldCleanAtIteration(
223
+ store: LunaticVariablesStore,
224
+ {
225
+ expressions,
226
+ iteration,
227
+ }: {
228
+ expressions: CleaningExpression[];
229
+ iteration?: number[];
190
230
  }
231
+ ) {
232
+ // Clean the variable if any condition is false (variable is not visible),
233
+ for (const expression of expressions) {
234
+ if (
235
+ !store.run(expression.expression, {
236
+ iteration,
237
+ })
238
+ )
239
+ return true;
240
+ }
241
+
242
+ // All conditions are true, no cleaning needed
243
+ return false;
191
244
  }
192
245
 
193
246
  /**
@@ -572,6 +572,65 @@ describe('lunatic-variables-store', () => {
572
572
  variables.set('PRENOM', ['John', 'Jane']);
573
573
  expect(variables.get('AGE')).toEqual([null, null]);
574
574
  });
575
+ it('should check every iteration when configured from an iterated source change', () => {
576
+ // Given
577
+ variables.set('PRENOM', ['Marc', 'Marc', 'Marc']);
578
+ variables.set('QCU', ['A', 'B', 'B']);
579
+ cleaningBehaviour(
580
+ variables,
581
+ {
582
+ PRENOM: {
583
+ QCU: [
584
+ {
585
+ expression: 'PRENOM <> "Marc"',
586
+ shapeFrom: 'PRENOM',
587
+ isAggregatorUsed: false,
588
+ shouldCheckAllIterations: true,
589
+ },
590
+ ],
591
+ },
592
+ },
593
+ { PRENOM: [], QCU: [null] }
594
+ );
595
+
596
+ // When
597
+ variables.set('PRENOM', 'Patrick', { iteration: [0] });
598
+
599
+ // Then
600
+ expect(variables.get('QCU')).toEqual(['A', null, null]);
601
+ });
602
+ it('should handle simultaneously expressions that should check every iteration and expression that should not ', () => {
603
+ // Given
604
+ variables.set('PRENOM', ['Marc', 'Marc', 'Marc']);
605
+ variables.set('QCU', ['A', 'B', 'B']);
606
+ cleaningBehaviour(
607
+ variables,
608
+ {
609
+ PRENOM: {
610
+ QCU: [
611
+ {
612
+ expression: 'PRENOM <> ""',
613
+ shapeFrom: 'PRENOM',
614
+ isAggregatorUsed: false,
615
+ },
616
+ {
617
+ expression: 'PRENOM <> "Marc"',
618
+ shapeFrom: 'PRENOM',
619
+ isAggregatorUsed: false,
620
+ shouldCheckAllIterations: true,
621
+ },
622
+ ],
623
+ },
624
+ },
625
+ { PRENOM: [], QCU: [null] }
626
+ );
627
+
628
+ // When
629
+ variables.set('PRENOM', '', { iteration: [0] });
630
+
631
+ // Then
632
+ expect(variables.get('QCU')).toEqual([null, null, null]);
633
+ });
575
634
  it('should clean pairwise in two directions', () => {
576
635
  variables.set('LINKS', [
577
636
  [null, '1', '3'],