project-booster-vue 8.115.7 → 8.116.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-booster-vue",
3
- "version": "8.115.7",
3
+ "version": "8.116.0",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "test:unit": "vue-cli-service test:unit --forceExit --detectOpenHandles",
@@ -68,22 +68,6 @@ export default {
68
68
  nextStep: null,
69
69
  });
70
70
  },
71
- computeNextStep(nextStep) {
72
- let resultingNextStep;
73
-
74
- if (nextStep?.conditionals) {
75
- for (const conditional of nextStep.conditionals) {
76
- if (this.areConditionsValid(conditional.conditions)) {
77
- resultingNextStep = this.computeNextStep(conditional.nextStep);
78
- break;
79
- }
80
- }
81
- } else {
82
- resultingNextStep = nextStep;
83
- }
84
-
85
- return resultingNextStep;
86
- },
87
71
  },
88
72
  };
89
73
  </script>
@@ -257,6 +257,7 @@ import MLink from '../mozaic/link/MLink';
257
257
  import PbCard from '../cards/PbCard';
258
258
  import PbStickyFooter from '../sticky-footer/PbStickyFooter';
259
259
  import { sortAnswers } from './sortAnswers';
260
+ import { areConditionsValid } from '../../service/scenarioConditionals';
260
261
 
261
262
  const BACK_ICON =
262
263
  'https://storage.googleapis.com/project-booster-media/mozaic-icons/svg/Navigation_Arrow_Arrow--Left_16px.svg';
@@ -391,7 +392,7 @@ export default {
391
392
 
392
393
  if (this.payload?.answers) {
393
394
  this.displayableAnswers = Object.values(this.payload?.answers).filter((answer) => {
394
- return this.areConditionsValid(answer.conditions);
395
+ return areConditionsValid(answer.conditions, this.answers, this.runtimeOptions);
395
396
  });
396
397
  }
397
398
 
@@ -406,7 +407,7 @@ export default {
406
407
  for (const answerCode in this.payload.answers) {
407
408
  if (
408
409
  this.payload.answers[answerCode].selected &&
409
- this.areConditionsValid(this.payload.answers[answerCode].conditions)
410
+ areConditionsValid(this.payload.answers[answerCode].conditions, this.answers, this.runtimeOptions)
410
411
  ) {
411
412
  this.selectedAnswers[answerCode] = true;
412
413
  }
@@ -445,35 +446,14 @@ export default {
445
446
  );
446
447
  },
447
448
  isShowingFooter(viewModel) {
448
- return viewModel.footer && this.areConditionsValid(viewModel.footer.conditions);
449
+ return viewModel.footer && areConditionsValid(viewModel.footer.conditions, this.answers, this.runtimeOptions);
449
450
  },
450
- areConditionsValid(conditions) {
451
- let valid = true;
452
-
453
- if (conditions) {
454
- valid = false;
455
- for (const condition of conditions) {
456
- try {
457
- let conditionValid = new Function(
458
- 'isAnswerMatching',
459
- 'isAnswerContaining',
460
- 'answers',
461
- 'runtimeOptions',
462
- `return ${condition}`,
463
- ).call(this, this.isAnswerMatching, this.isAnswerContaining, this.answers, this.runtimeOptions);
464
-
465
- valid = valid || conditionValid;
466
- } catch (error) {
467
- console.error(error);
468
- valid = valid || true;
469
- }
470
- }
471
- }
472
451
 
473
- return valid;
474
- },
475
452
  isAnswerDisabled(answer) {
476
- return answer.viewModel.disabled && this.areConditionsValid(answer.viewModel.disabled.conditions);
453
+ return (
454
+ answer.viewModel.disabled &&
455
+ areConditionsValid(answer.viewModel.disabled.conditions, this.answers, this.runtimeOptions)
456
+ );
477
457
  },
478
458
  selectAnswer(stepCode, answer) {
479
459
  if (this.isAnswerDisabled(answer)) {
@@ -499,7 +479,7 @@ export default {
499
479
  */
500
480
  this.$emit(this.completedEventName, {
501
481
  answers: [answer],
502
- nextStep: this.computeNextStep(answer.nextStep),
482
+ nextStep: answer.nextStep,
503
483
  });
504
484
  }
505
485
 
@@ -527,18 +507,6 @@ export default {
527
507
 
528
508
  return selectedAnswersNumber;
529
509
  },
530
- // Function used by the scenario conditions
531
- isAnswerMatching(questionCode, answerCode) {
532
- return this?.answers[questionCode] && this?.answers[questionCode][0]?.code === answerCode;
533
- },
534
- isAnswerContaining(questionCode, answerCode) {
535
- return (
536
- this?.answers[questionCode] &&
537
- this?.answers[questionCode].findIndex((elt) => {
538
- return elt.code === answerCode;
539
- }) > -1
540
- );
541
- },
542
510
  validMultiSelect(multiSelectOptions) {
543
511
  const answers = [];
544
512
 
@@ -550,14 +518,14 @@ export default {
550
518
 
551
519
  this.$emit(this.completedEventName, {
552
520
  answers: answers,
553
- nextStep: this.computeNextStep(multiSelectOptions.nextStep),
521
+ nextStep: multiSelectOptions.nextStep,
554
522
  });
555
523
  },
556
524
  skipQuestion(button) {
557
525
  this.initAnswersSelectedState(this.payload.answers);
558
526
  this.$emit(this.completedEventName, {
559
- answers: button.defaultAnswer ? [button.defaultAnswer] : [],
560
- nextStep: this.computeNextStep(button.nextStep),
527
+ answers: button.isAnswer ? (button.defaultAnswer ? [button.defaultAnswer] : []) : null,
528
+ nextStep: button.nextStep,
561
529
  });
562
530
  },
563
531
  resetMultiSelect(multiSelectOptions, answers) {
@@ -569,22 +537,6 @@ export default {
569
537
  answer.selected = false;
570
538
  });
571
539
  },
572
- computeNextStep(nextStep) {
573
- let resultingNextStep;
574
-
575
- if (nextStep?.conditionals) {
576
- for (const conditional of nextStep.conditionals) {
577
- if (this.areConditionsValid(conditional.conditions)) {
578
- resultingNextStep = this.computeNextStep(conditional.nextStep);
579
- break;
580
- }
581
- }
582
- } else {
583
- resultingNextStep = nextStep;
584
- }
585
-
586
- return resultingNextStep;
587
- },
588
540
  handleShowMoreClick() {
589
541
  this.activateShowMoreAnimation();
590
542
  this.showMore();
@@ -282,7 +282,11 @@ export default {
282
282
  * @type {Event}
283
283
  */
284
284
  emit(props.completedEventName, {
285
- answers: props.payload?.skippable?.defaultAnswer ? [props.payload?.skippable?.defaultAnswer] : [],
285
+ answers: props.payload?.skippable?.isAnswer
286
+ ? props.payload?.skippable?.defaultAnswer
287
+ ? [props.payload?.skippable?.defaultAnswer]
288
+ : []
289
+ : null,
286
290
  nextStep: props.payload?.skippable?.nextStep,
287
291
  });
288
292
  };
@@ -54,7 +54,7 @@
54
54
  @click="skipQuestion"
55
55
  />
56
56
  <m-button
57
- v-if="hasSelectedAnswers || payload.multiSelect.alwaysDisplayMultiSelectButton"
57
+ v-if="hasSelectedAnswers || payload.multiSelect?.alwaysDisplayMultiSelectButton"
58
58
  class="pb-list-select__validate-button"
59
59
  size="m"
60
60
  size-from-l="l"
@@ -204,7 +204,7 @@ export default {
204
204
  return props.payload?.viewModel?.showMore?.position ?? 'center';
205
205
  });
206
206
 
207
- // Submition
207
+ // Submit
208
208
  const validateButtonProps = computed(() => {
209
209
  let label, leftIcon, rightIcon;
210
210
 
@@ -253,52 +253,10 @@ export default {
253
253
  */
254
254
  emit(props.completedEventName, {
255
255
  answers: answersToSubmit,
256
- nextStep: computeNextStep(nextStep),
256
+ nextStep: nextStep,
257
257
  });
258
258
  };
259
259
 
260
- // Scenario management
261
- const areConditionsValid = (conditions) => {
262
- let valid = true;
263
-
264
- if (conditions) {
265
- valid = false;
266
- for (const condition of conditions) {
267
- try {
268
- let conditionValid = new Function(
269
- 'isAnswerMatching',
270
- 'isAnswerContaining',
271
- 'answers',
272
- 'runtimeOptions',
273
- `return ${condition}`,
274
- ).call(this, isAnswerMatching, isAnswerContaining, props.answers, props.runtimeOptions);
275
-
276
- valid = valid || conditionValid;
277
- } catch (error) {
278
- console.error(error);
279
- valid = valid || true;
280
- }
281
- }
282
- }
283
- return valid;
284
- };
285
-
286
- const computeNextStep = (nextStep) => {
287
- let resultingNextStep;
288
-
289
- if (nextStep?.conditionals) {
290
- for (const conditional of nextStep.conditionals) {
291
- if (areConditionsValid(conditional.conditions)) {
292
- resultingNextStep = computeNextStep(conditional.nextStep);
293
- break;
294
- }
295
- }
296
- } else {
297
- resultingNextStep = nextStep;
298
- }
299
- return resultingNextStep;
300
- };
301
-
302
260
  const skipQuestion = () => {
303
261
  /**
304
262
  * Emitted when step is completed
@@ -306,8 +264,12 @@ export default {
306
264
  * @type {Event}
307
265
  */
308
266
  emit(props.completedEventName, {
309
- answers: props.payload.skippable.defaultAnswer ? [props.payload.skippable.defaultAnswer.value] : [],
310
- nextStep: computeNextStep(props.payload?.skippable?.nextStep),
267
+ answers: props.payload?.skippable?.isAnswer
268
+ ? props.payload?.skippable?.defaultAnswer
269
+ ? [props.payload?.skippable?.defaultAnswer]
270
+ : []
271
+ : null,
272
+ nextStep: props.payload?.skippable?.nextStep,
311
273
  });
312
274
  };
313
275
 
@@ -322,19 +284,6 @@ export default {
322
284
  );
323
285
  };
324
286
 
325
- const isAnswerMatching = (questionCode, answerCode) => {
326
- return props.answers[questionCode] && props.answers[questionCode][0]?.code === answerCode;
327
- };
328
-
329
- const isAnswerContaining = (questionCode, answerCode) => {
330
- return (
331
- props.answers[questionCode] &&
332
- props.answers[questionCode].findIndex((elt) => {
333
- return elt.code === answerCode;
334
- }) > -1
335
- );
336
- };
337
-
338
287
  const getAnswerValue = (answerCode, path) => {
339
288
  if (!props.answers[answerCode] || props.answers[answerCode].length === 0) {
340
289
  return null;
@@ -249,7 +249,11 @@ export default {
249
249
  * @type {Event}
250
250
  */
251
251
  emit(props.completedEventName, {
252
- answers: props.payload?.skippable?.defaultAnswer ? [props.payload?.skippable?.defaultAnswer] : [],
252
+ answers: props.payload?.skippable?.isAnswer
253
+ ? props.payload?.skippable?.defaultAnswer
254
+ ? [props.payload?.skippable?.defaultAnswer]
255
+ : []
256
+ : null,
253
257
  nextStep: props.payload?.skippable?.nextStep,
254
258
  });
255
259
  };
@@ -186,11 +186,10 @@ import MIcon from '../../mozaic/icon/MIcon';
186
186
  import MImage from '../../mozaic/image/MImage';
187
187
  import MLink from '../../mozaic/link/MLink';
188
188
  import PbCard from '../../cards/PbCard';
189
- import { sortAnswers } from '.././sortAnswers';
190
189
  import MDialog from '../../mozaic/dialog/MDialog';
191
- import { v4 as uuidv4 } from 'uuid';
192
190
  import PbUploadDocumentForm from './PbUploadDocumentForm';
193
191
  import PbStickyFooter from '../../sticky-footer/PbStickyFooter';
192
+ import { areConditionsValid } from '../../../service/scenarioConditionals';
194
193
 
195
194
  const BACK_ICON =
196
195
  'https://storage.googleapis.com/project-booster-media/mozaic-icons/svg/Navigation_Arrow_Arrow--Left_16px.svg';
@@ -327,7 +326,7 @@ export default {
327
326
  updateDefaultAnswers() {
328
327
  if (this.payload?.answers) {
329
328
  this.displayableAnswers = Object.values(this.payload?.answers).filter((answer) => {
330
- return this.areConditionsValid(answer.conditions) && !answer.selected;
329
+ return areConditionsValid(answer.conditions, this.answers, this.runtimeOptions) && !answer.selected;
331
330
  });
332
331
  Object.values(this.payload?.answers).map((answer) => {
333
332
  let found = false;
@@ -371,42 +370,20 @@ export default {
371
370
  );
372
371
  },
373
372
  isShowingFooter(viewModel) {
374
- return viewModel.footer && this.areConditionsValid(viewModel.footer.conditions);
375
- },
376
- areConditionsValid(conditions) {
377
- let valid = true;
378
-
379
- if (conditions) {
380
- valid = false;
381
- for (const condition of conditions) {
382
- try {
383
- let conditionValid = new Function(
384
- 'isAnswerMatching',
385
- 'isAnswerContaining',
386
- 'answers',
387
- 'runtimeOptions',
388
- `return ${condition}`,
389
- ).call(this, this.isAnswerMatching, this.isAnswerContaining, this.answers, this.runtimeOptions);
390
-
391
- valid = valid || conditionValid;
392
- } catch (error) {
393
- console.error(error);
394
- valid = valid || true;
395
- }
396
- }
397
- }
398
-
399
- return valid;
373
+ return viewModel.footer && areConditionsValid(viewModel.footer.conditions, this.answers, this.runtimeOptions);
400
374
  },
375
+
401
376
  isAnswerDisabled(answer) {
402
- return answer.viewModel.disabled && this.areConditionsValid(answer.viewModel.disabled.conditions);
377
+ return (
378
+ answer.viewModel.disabled &&
379
+ areConditionsValid(answer.viewModel.disabled.conditions, this.answers, this.runtimeOptions)
380
+ );
403
381
  },
404
382
  selectAnswer(stepCode, answer) {
405
383
  if (answer.code === 'ADD_DOCUMENT') {
406
384
  this.$refs.pbUploadDocumentForm.$refs.pbDocumentUploadFormInput.click();
407
385
  }
408
386
  },
409
- // Function used by the scenario conditions
410
387
  getAnswerValue(answerCode, path) {
411
388
  if (!this.answers[answerCode] || this.answers[answerCode].length === 0) {
412
389
  return null;
@@ -414,28 +391,16 @@ export default {
414
391
 
415
392
  return objectPath.get(this.answers[answerCode][0], path);
416
393
  },
417
- // Function used by the scenario conditions
418
- isAnswerMatching(questionCode, answerCode) {
419
- return this?.answers[questionCode] && this?.answers[questionCode][0]?.code === answerCode;
420
- },
421
- isAnswerContaining(questionCode, answerCode) {
422
- return (
423
- this?.answers[questionCode] &&
424
- this?.answers[questionCode].findIndex((elt) => {
425
- return elt.code === answerCode;
426
- }) > -1
427
- );
428
- },
429
394
  validMultiSelect(multiSelectOptions) {
430
395
  this.$emit(this.completedEventName, {
431
396
  answers: this.selectedDocuments,
432
- nextStep: this.computeNextStep(multiSelectOptions.nextStep),
397
+ nextStep: multiSelectOptions.nextStep,
433
398
  });
434
399
  },
435
400
  skipQuestion(button) {
436
401
  this.$emit(this.completedEventName, {
437
402
  answers: this.selectedDocuments,
438
- nextStep: this.computeNextStep(button.nextStep),
403
+ nextStep: button.nextStep,
439
404
  });
440
405
  },
441
406
  resetMultiSelect(multiSelectOptions, answers) {
@@ -450,22 +415,6 @@ export default {
450
415
  answer.selected = false;
451
416
  });
452
417
  },
453
- computeNextStep(nextStep) {
454
- let resultingNextStep;
455
-
456
- if (nextStep?.conditionals) {
457
- for (const conditional of nextStep.conditionals) {
458
- if (this.areConditionsValid(conditional.conditions)) {
459
- resultingNextStep = this.computeNextStep(conditional.nextStep);
460
- break;
461
- }
462
- }
463
- } else {
464
- resultingNextStep = nextStep;
465
- }
466
-
467
- return resultingNextStep;
468
- },
469
418
  handleShowMoreClick() {
470
419
  this.activateShowMoreAnimation();
471
420
  this.showMore();
@@ -4,7 +4,7 @@
4
4
  :class="`pb-restitution pb-restitution--${componentTheme}`"
5
5
  align-items="center"
6
6
  direction="column"
7
- :style="`min-height: ${displayRestitutionExit ? 'auto' : minHeight};`"
7
+ :style="`min-height: ${restitutionMinHeight};`"
8
8
  ref="pbRestitution"
9
9
  >
10
10
  <m-link
@@ -78,6 +78,7 @@ import PbRestitutionBody from './PbRestitutionBody';
78
78
  import PbProgressionPrice from '../progression-price/PbProgressionPrice';
79
79
  import PbProjectItemSave from '../project-item-save/PbProjectItemSave';
80
80
  import PbRestitutionExit from './PbRestitutionExit';
81
+ import { areConditionsValid } from '../../service/scenarioConditionals';
81
82
 
82
83
  const BACK_ICON =
83
84
  'https://storage.googleapis.com/project-booster-media/mozaic-icons/svg/Navigation_Arrow_Arrow--Left_16px.svg';
@@ -165,6 +166,7 @@ export default {
165
166
  notifications: [],
166
167
  displayDialog: false,
167
168
  dialogViewModel: null,
169
+ restitutionMinHeight: '100vh',
168
170
  }),
169
171
 
170
172
  computed: {
@@ -184,7 +186,7 @@ export default {
184
186
  options: [],
185
187
  };
186
188
  formattedExitOptions.options = this.payload?.exitOptions?.options?.filter(
187
- (option) => !option.conditions || this.areConditionsValid(option.conditions),
189
+ (option) => !option.conditions || areConditionsValid(option.conditions, this.answers, this.runtimeOptions),
188
190
  );
189
191
  return formattedExitOptions;
190
192
  },
@@ -200,7 +202,7 @@ export default {
200
202
  if (this.payload?.callToActions) {
201
203
  const saveAction = Object.values(this.payload.callToActions).find((cta) => {
202
204
  return (
203
- this.areConditionsValid(cta.conditions) &&
205
+ areConditionsValid(cta.conditions, this.answers, this.runtimeOptions) &&
204
206
  cta.action?.type === 'MODAL' &&
205
207
  cta.action?.component === 'PbProjectItemSave'
206
208
  );
@@ -208,6 +210,9 @@ export default {
208
210
  this.projectSaveItemPayload = saveAction ? saveAction.payload : {};
209
211
  this.showSaveProjectItem = this.showSaveEstimate;
210
212
  }
213
+ setTimeout(() => {
214
+ this.restitutionMinHeight = this.payload?.exitOptions ? 'auto' : this.minHeight;
215
+ }, 300);
211
216
  },
212
217
 
213
218
  mounted() {
@@ -219,41 +224,11 @@ export default {
219
224
  updateDisplayedCta() {
220
225
  if (this.payload?.callToActions) {
221
226
  this.displayedCta = Object.values(this.payload.callToActions).filter((cta) => {
222
- return this.areConditionsValid(cta.conditions);
227
+ return areConditionsValid(cta.conditions, this.answers, this.runtimeOptions);
223
228
  });
224
229
  }
225
230
  },
226
231
 
227
- areConditionsValid(conditions) {
228
- let valid = true;
229
-
230
- if (conditions) {
231
- valid = false;
232
- for (const condition of conditions) {
233
- try {
234
- valid =
235
- valid ||
236
- new Function('isAnswerMatching', 'answers', 'runtimeOptions', `return ${condition}`).call(
237
- this,
238
- this.isAnswerMatching,
239
- this.answers,
240
- this.runtimeOptions,
241
- );
242
- } catch (error) {
243
- console.error(error);
244
- valid = valid || true;
245
- }
246
- }
247
- }
248
-
249
- return valid;
250
- },
251
-
252
- // Function used by the scenario conditions
253
- isAnswerMatching(questionCode, answerCode) {
254
- return this?.answers[questionCode] && this?.answers[questionCode][0]?.code === answerCode;
255
- },
256
-
257
232
  callAction(action) {
258
233
  if (action.type === 'MODAL') {
259
234
  if (action.component === 'PbProjectItemSave') {
@@ -72,6 +72,7 @@ import MLink from '../mozaic/link/MLink';
72
72
  import { priceFormatter } from '../../service/priceFormatter';
73
73
  import PbRestitutionBlock from './PbRestitutionBlock';
74
74
  import MNotification from '../mozaic/notifications/MNotification';
75
+ import { areConditionsValid } from '../../service/scenarioConditionals';
75
76
 
76
77
  export default {
77
78
  name: 'PbRestitutionBody',
@@ -186,44 +187,7 @@ export default {
186
187
  },
187
188
 
188
189
  areConditionsValid(conditions) {
189
- let valid = true;
190
-
191
- if (conditions) {
192
- valid = false;
193
- for (const condition of conditions) {
194
- try {
195
- let conditionValid = new Function(
196
- 'isAnswerMatching',
197
- 'isAnswerContaining',
198
- 'answers',
199
- 'runtimeOptions',
200
- `return ${condition}`,
201
- ).call(this, this.isAnswerMatching, this.isAnswerContaining, this.answers, this.runtimeOptions);
202
-
203
- valid = valid || conditionValid;
204
- } catch (error) {
205
- console.error(error);
206
- valid = valid || true;
207
- }
208
- }
209
- }
210
-
211
- return valid;
212
- },
213
-
214
- // Function used by the scenario conditions
215
- isAnswerMatching(questionCode, answerCode) {
216
- return this?.answers && this?.answers[questionCode] && this?.answers[questionCode][0]?.code === answerCode;
217
- },
218
-
219
- isAnswerContaining(questionCode, answerCode) {
220
- return (
221
- this?.answers &&
222
- this?.answers[questionCode] &&
223
- this?.answers[questionCode].findIndex((elt) => {
224
- return elt.code === answerCode;
225
- }) > -1
226
- );
190
+ return areConditionsValid(conditions, this.answers, this.runtimeOptions);
227
191
  },
228
192
  },
229
193
  };
@@ -0,0 +1,325 @@
1
+ import { nestedAppDecorator } from '../../../.storybook/nested-app-decorator';
2
+ import { Anchor, Story, Preview, Meta, Props, ArgsTable, Source, Canvas } from '@storybook/addon-docs';
3
+ import PbScenario from './PbScenario';
4
+ import DEFAULT_PAYLOAD from './default-payload.json';
5
+ import { TemplateSandbox } from './PbScenario.stories.mdx';
6
+
7
+ <Meta
8
+ title="Project Booster/Scenario/ 🦠 PbScenario/Features/skippable answers"
9
+ component={PbScenario}
10
+ argTypes={{
11
+ scenarios: {
12
+ table: {
13
+ defaultValue: {
14
+ summary: 'object',
15
+ detail: JSON.stringify(DEFAULT_PAYLOAD, null, ' '),
16
+ },
17
+ },
18
+ control: {
19
+ type: 'object',
20
+ },
21
+ },
22
+ onScenarioCompleted: {
23
+ action: 'Scenario completed',
24
+ table: { disable: true },
25
+ },
26
+ onScenarioEvent: {
27
+ action: 'Scenario event occured',
28
+ table: { disable: true },
29
+ },
30
+ }}
31
+ decorators={[nestedAppDecorator({}, [{ path: '/steps/:stepCode/previous/:answers', component: PbScenario }])]}
32
+ parameters={{
33
+ layout: 'fullscreen',
34
+ }}
35
+ />
36
+
37
+ <Anchor storyId="project-booster-scenario-pbscenario-features--skippable-scenario" />
38
+
39
+ ## Skippable with answer
40
+
41
+ Conditions can be applied to next steps:
42
+
43
+ ```json
44
+ {
45
+ "skippable": [
46
+ "isAnswer":true,
47
+ {
48
+ "label": "Skip"
49
+ }
50
+ ],
51
+ }
52
+ ```
53
+
54
+ When the isAnswer is true the step-completed sended and the state history updated
55
+
56
+ export const skippableEventWithDefaultAnswerPayload = {
57
+ '__START__': {
58
+ code: '__START__',
59
+ type: 'SCENARIO',
60
+ stepCode: 'MAIN_SCENARIO',
61
+ },
62
+ 'MAIN_SCENARIO': {
63
+ code: 'MAIN_SCENARIO',
64
+ type: 'SCENARIO',
65
+ stepCode: 'SCENARIO-1',
66
+ },
67
+ 'SCENARIO-1': {
68
+ code: 'SCENARIO-1',
69
+ type: 'STEP',
70
+ component: 'PbQuestion',
71
+ payload: {
72
+ skippable: {
73
+ isAnswer: true,
74
+ defaultAnswer: {
75
+ code: 'NONE',
76
+ value: 'NONE',
77
+ },
78
+ label: 'Skip',
79
+ theme: 'bordered',
80
+ rightIcon:
81
+ 'https://storage.googleapis.com/project-booster-media/mozaic-icons/svg/Navigation_Arrow_Arrow--Right_24px.svg',
82
+ leftIcon:
83
+ 'https://storage.googleapis.com/project-booster-media/mozaic-icons/svg/Navigation_Arrow_Arrow--Left_24px.svg',
84
+ nextStep: {
85
+ code: '__END__',
86
+ },
87
+ },
88
+ viewModel: {
89
+ label: 'The question title',
90
+ answersComponent: 'PbCard',
91
+ },
92
+ answers: {
93
+ 'ANSWER-1': {
94
+ code: 'ANSWER-1',
95
+ viewModel: {
96
+ title: 'Answer 1',
97
+ },
98
+ },
99
+ 'ANSWER-2': {
100
+ code: 'ANSWER-2',
101
+ viewModel: {
102
+ title: 'Answer 2',
103
+ },
104
+ },
105
+ 'ANSWER-3': {
106
+ code: 'ANSWER-3',
107
+ viewModel: {
108
+ title: 'Answer 3',
109
+ },
110
+ },
111
+ 'ANSWER-4': {
112
+ code: 'ANSWER-4',
113
+ viewModel: {
114
+ title: 'Answer 4',
115
+ },
116
+ },
117
+ },
118
+ },
119
+ nextStep: {
120
+ code: '__END__',
121
+ },
122
+ },
123
+ '__END__': {
124
+ code: '__END__',
125
+ type: 'STEP',
126
+ component: 'PbQuestion',
127
+ payload: {
128
+ viewModel: {
129
+ label: 'Fin',
130
+ },
131
+ },
132
+ },
133
+ };
134
+
135
+ <Source language="json" code={JSON.stringify(skippableEventWithDefaultAnswerPayload, null, ' ')} />
136
+
137
+ <Canvas>
138
+ <Story
139
+ name="Skippable event with default answer"
140
+ inline={false}
141
+ height="862px"
142
+ parameters={{ controls: { disable: true } }}
143
+ args={{ scenarios: skippableEventWithDefaultAnswerPayload, minHeight: '120vh' }}
144
+ >
145
+ {TemplateSandbox.bind({})}
146
+ </Story>
147
+ </Canvas>
148
+
149
+ export const skippableWithoutEventQuestionPayload = {
150
+ '__START__': {
151
+ code: '__START__',
152
+ type: 'SCENARIO',
153
+ stepCode: 'MAIN_SCENARIO',
154
+ },
155
+ 'MAIN_SCENARIO': {
156
+ code: 'MAIN_SCENARIO',
157
+ type: 'SCENARIO',
158
+ stepCode: 'SCENARIO-1',
159
+ },
160
+ 'SCENARIO-1': {
161
+ code: 'SCENARIO-1',
162
+ type: 'STEP',
163
+ component: 'PbQuestion',
164
+ payload: {
165
+ skippable: {
166
+ isAnswer: false,
167
+ label: 'Skip',
168
+ theme: 'bordered',
169
+ rightIcon:
170
+ 'https://storage.googleapis.com/project-booster-media/mozaic-icons/svg/Navigation_Arrow_Arrow--Right_24px.svg',
171
+ leftIcon:
172
+ 'https://storage.googleapis.com/project-booster-media/mozaic-icons/svg/Navigation_Arrow_Arrow--Left_24px.svg',
173
+ nextStep: {
174
+ code: '__END__',
175
+ },
176
+ },
177
+ viewModel: {
178
+ label: 'The question title',
179
+ answersComponent: 'PbCard',
180
+ },
181
+ answers: {
182
+ 'ANSWER-1': {
183
+ code: 'ANSWER-1',
184
+ viewModel: {
185
+ title: 'Answer 1',
186
+ },
187
+ },
188
+ 'ANSWER-2': {
189
+ code: 'ANSWER-2',
190
+ viewModel: {
191
+ title: 'Answer 2',
192
+ },
193
+ },
194
+ 'ANSWER-3': {
195
+ code: 'ANSWER-3',
196
+ viewModel: {
197
+ title: 'Answer 3',
198
+ },
199
+ },
200
+ 'ANSWER-4': {
201
+ code: 'ANSWER-4',
202
+ viewModel: {
203
+ title: 'Answer 4',
204
+ },
205
+ },
206
+ },
207
+ },
208
+ nextStep: {
209
+ code: '__END__',
210
+ },
211
+ },
212
+ '__END__': {
213
+ code: '__END__',
214
+ type: 'STEP',
215
+ component: 'PbQuestion',
216
+ payload: {
217
+ viewModel: {
218
+ label: 'Fin',
219
+ },
220
+ },
221
+ },
222
+ };
223
+
224
+ <Source language="json" code={JSON.stringify(skippableWithoutEventQuestionPayload, null, ' ')} />
225
+
226
+ <Canvas>
227
+ <Story
228
+ name="Skippable without event"
229
+ inline={false}
230
+ height="862px"
231
+ parameters={{ controls: { disable: true } }}
232
+ args={{ scenarios: skippableWithoutEventQuestionPayload, minHeight: '120vh' }}
233
+ >
234
+ {TemplateSandbox.bind({})}
235
+ </Story>
236
+ </Canvas>
237
+
238
+ export const skippableEventWithEmptyArrayPayload = {
239
+ '__START__': {
240
+ code: '__START__',
241
+ type: 'SCENARIO',
242
+ stepCode: 'MAIN_SCENARIO',
243
+ },
244
+ 'MAIN_SCENARIO': {
245
+ code: 'MAIN_SCENARIO',
246
+ type: 'SCENARIO',
247
+ stepCode: 'SCENARIO-1',
248
+ },
249
+ 'SCENARIO-1': {
250
+ code: 'SCENARIO-1',
251
+ type: 'STEP',
252
+ component: 'PbQuestion',
253
+ payload: {
254
+ skippable: {
255
+ isAnswer: true,
256
+ label: 'Skip',
257
+ theme: 'bordered',
258
+ rightIcon:
259
+ 'https://storage.googleapis.com/project-booster-media/mozaic-icons/svg/Navigation_Arrow_Arrow--Right_24px.svg',
260
+ leftIcon:
261
+ 'https://storage.googleapis.com/project-booster-media/mozaic-icons/svg/Navigation_Arrow_Arrow--Left_24px.svg',
262
+ nextStep: {
263
+ code: '__END__',
264
+ },
265
+ },
266
+ viewModel: {
267
+ label: 'The question title',
268
+ answersComponent: 'PbCard',
269
+ },
270
+ answers: {
271
+ 'ANSWER-1': {
272
+ code: 'ANSWER-1',
273
+ viewModel: {
274
+ title: 'Answer 1',
275
+ },
276
+ },
277
+ 'ANSWER-2': {
278
+ code: 'ANSWER-2',
279
+ viewModel: {
280
+ title: 'Answer 2',
281
+ },
282
+ },
283
+ 'ANSWER-3': {
284
+ code: 'ANSWER-3',
285
+ viewModel: {
286
+ title: 'Answer 3',
287
+ },
288
+ },
289
+ 'ANSWER-4': {
290
+ code: 'ANSWER-4',
291
+ viewModel: {
292
+ title: 'Answer 4',
293
+ },
294
+ },
295
+ },
296
+ },
297
+ nextStep: {
298
+ code: '__END__',
299
+ },
300
+ },
301
+ '__END__': {
302
+ code: '__END__',
303
+ type: 'STEP',
304
+ component: 'PbQuestion',
305
+ payload: {
306
+ viewModel: {
307
+ label: 'Fin',
308
+ },
309
+ },
310
+ },
311
+ };
312
+
313
+ <Source language="json" code={JSON.stringify(skippableEventWithEmptyArrayPayload, null, ' ')} />
314
+
315
+ <Canvas>
316
+ <Story
317
+ name="Skippable event with empty array"
318
+ inline={false}
319
+ height="862px"
320
+ parameters={{ controls: { disable: true } }}
321
+ args={{ scenarios: skippableEventWithEmptyArrayPayload, minHeight: '120vh' }}
322
+ >
323
+ {TemplateSandbox.bind({})}
324
+ </Story>
325
+ </Canvas>
@@ -89,6 +89,7 @@ import PbRestitution from '../restitution/PbRestitution';
89
89
  import PbSmartProgressionPrice from '../progression-price/PbSmartProgressionPrice';
90
90
  import PbSpaceInput from '../question/space-input/PbSpaceInput';
91
91
  import PbUploadDocument from '../question/upload-document/PbUploadDocument';
92
+ import { areConditionsValid } from '../../service/scenarioConditionals';
92
93
 
93
94
  export default {
94
95
  name: 'PbScenario',
@@ -301,8 +302,10 @@ export default {
301
302
  };
302
303
  const handleStepCompleted = ({ answers, nextStep }) => {
303
304
  disablePointerEvents();
304
- state.value.answers[state.value.displayedStep.code] = answers;
305
- emitEventToPipeline('STEP-COMPLETED', answers);
305
+ if (answers) {
306
+ state.value.answers[state.value.displayedStep.code] = answers;
307
+ emitEventToPipeline('STEP-COMPLETED', answers);
308
+ }
306
309
 
307
310
  if (!nextStep) {
308
311
  nextStep = state.value.displayedStep?.nextStep;
@@ -444,7 +447,7 @@ export default {
444
447
 
445
448
  if (nextStep?.conditionals) {
446
449
  for (const conditional of nextStep.conditionals) {
447
- if (areConditionsValid(conditional.conditions)) {
450
+ if (areConditionsValid(conditional.conditions, state.value.answers, props.runtimeOptions)) {
448
451
  resultingNextStep = computeNextStep(conditional.nextStep);
449
452
  break;
450
453
  }
@@ -455,42 +458,7 @@ export default {
455
458
 
456
459
  return resultingNextStep;
457
460
  };
458
- const areConditionsValid = (conditions) => {
459
- let valid = true;
460
-
461
- if (conditions) {
462
- valid = false;
463
- for (const condition of conditions) {
464
- valid =
465
- valid ||
466
- new Function(
467
- 'isAnswerMatching',
468
- 'isAnswerContaining',
469
- 'answers',
470
- 'runtimeOptions',
471
- `return ${condition}`,
472
- ).call(this, isAnswerMatching, isAnswerContaining, state.value.answers, props.runtimeOptions);
473
- }
474
- }
475
461
 
476
- return valid;
477
- };
478
- // Function used by the scenario conditions
479
- const isAnswerMatching = (questionCode, answerCode) => {
480
- return (
481
- state.value.answers[questionCode] &&
482
- state.value.answers[questionCode][0] &&
483
- state.value.answers[questionCode][0].code === answerCode
484
- );
485
- };
486
- const isAnswerContaining = (questionCode, answerCode) => {
487
- return (
488
- state.value.answers[questionCode] &&
489
- state.value.answers[questionCode].findIndex((elt) => {
490
- return elt.code === answerCode;
491
- }) > -1
492
- );
493
- };
494
462
  const stepAnimationName = () => {
495
463
  const componentName = state.value.currentStep?.component;
496
464
  let animationName = 'pb-scenario__step--slide';
@@ -295,6 +295,7 @@
295
295
  },
296
296
  "skippable": [
297
297
  {
298
+ "isAnswer": true,
298
299
  "label": "Ajouter un plan plus tard",
299
300
  "theme": "text-primary",
300
301
  "width": "full",
@@ -304,6 +305,7 @@
304
305
  }
305
306
  },
306
307
  {
308
+ "isAnswer": true,
307
309
  "label": "Ajouter un plan",
308
310
  "theme": "solid",
309
311
  "width": "full",
@@ -403,6 +405,7 @@
403
405
  },
404
406
  "skippable": [
405
407
  {
408
+ "isAnswer": true,
406
409
  "label": "Ajouter un plan plus tard",
407
410
  "theme": "text-primary",
408
411
  "width": "full",
@@ -0,0 +1,44 @@
1
+ function Condition(answers, runtimeOptions, condition) {
2
+ this.answers = answers;
3
+ this.runtimeOptions = runtimeOptions;
4
+ this.condition = condition;
5
+
6
+ this.isAnswerMatching = (questionCode, answerCode) => {
7
+ return this.answers[questionCode] && this.answers[questionCode][0]?.code === answerCode;
8
+ };
9
+
10
+ this.isAnswerContaining = (questionCode, answerCode) => {
11
+ return (
12
+ this.answers[questionCode] &&
13
+ this.answers[questionCode].findIndex((elt) => {
14
+ return elt.code === answerCode;
15
+ }) > -1
16
+ );
17
+ };
18
+
19
+ this.isValid = new Function(
20
+ 'isAnswerMatching',
21
+ 'isAnswerContaining',
22
+ 'answers',
23
+ 'runtimeOptions',
24
+ `return ${condition}`,
25
+ ).call(this, this.isAnswerMatching, this.isAnswerContaining, this.answers, this.runtimeOptions);
26
+ }
27
+
28
+ export const areConditionsValid = (conditions, answers, runtimeOptions) => {
29
+ let valid = true;
30
+
31
+ if (conditions) {
32
+ valid = false;
33
+ for (const condition of conditions) {
34
+ try {
35
+ valid = valid || new Condition(answers, runtimeOptions, condition).isValid;
36
+ } catch (error) {
37
+ console.error(error);
38
+ valid = valid || true;
39
+ }
40
+ }
41
+ }
42
+
43
+ return valid;
44
+ };
@@ -164,6 +164,7 @@ export default {
164
164
  formattedScenario[key].payload.viewModel['alwaysDisplaySkippable'] = true;
165
165
  formattedScenario[key].payload['skippable'] = [
166
166
  {
167
+ isAnswer: true,
167
168
  defaultAnswer: {
168
169
  code: selectedAnswers[key][0]['code'],
169
170
  },