valtech-components 4.0.1006 → 4.0.1007

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 (21) hide show
  1. package/esm2022/lib/components/organisms/field-schema-editor/field-schema-editor.component.mjs +14 -1
  2. package/esm2022/lib/components/organisms/field-schema-editor/field-schema-editor.i18n.mjs +5 -1
  3. package/esm2022/lib/components/organisms/field-schema-editor/types.mjs +1 -1
  4. package/esm2022/lib/components/organisms/survey-builder/survey-builder.component.mjs +70 -7
  5. package/esm2022/lib/components/organisms/survey-builder/survey-builder.i18n.mjs +9 -1
  6. package/esm2022/lib/components/organisms/survey-builder/survey-preview-modal.component.mjs +129 -0
  7. package/esm2022/lib/components/organisms/survey-response/survey-response.component.mjs +135 -5
  8. package/esm2022/lib/components/organisms/survey-response/survey-response.i18n.mjs +7 -1
  9. package/esm2022/lib/services/forms/form-schema-builder.service.mjs +18 -4
  10. package/esm2022/lib/services/forms/types.mjs +1 -1
  11. package/esm2022/lib/version.mjs +2 -2
  12. package/fesm2022/valtech-components.mjs +372 -13
  13. package/fesm2022/valtech-components.mjs.map +1 -1
  14. package/lib/components/organisms/field-schema-editor/field-schema-editor.component.d.ts +1 -0
  15. package/lib/components/organisms/field-schema-editor/types.d.ts +2 -0
  16. package/lib/components/organisms/survey-builder/survey-builder.component.d.ts +5 -0
  17. package/lib/components/organisms/survey-builder/survey-preview-modal.component.d.ts +28 -0
  18. package/lib/components/organisms/survey-response/survey-response.component.d.ts +11 -1
  19. package/lib/services/forms/types.d.ts +6 -0
  20. package/lib/version.d.ts +1 -1
  21. package/package.json +1 -1
@@ -70,7 +70,7 @@ import fixWebmDuration from 'fix-webm-duration';
70
70
  * Current version of valtech-components.
71
71
  * This is automatically updated during the publish process.
72
72
  */
73
- const VERSION = '4.0.1006';
73
+ const VERSION = '4.0.1007';
74
74
 
75
75
  function evaluateValtechAccess(rule, context, features = {}, visitedFeatures = new Set()) {
76
76
  if (rule == null)
@@ -39116,11 +39116,22 @@ class FormSchemaBuilderService {
39116
39116
  }
39117
39117
  buildForm(schema, i18nNamespace, formName, submitLabelKey = 'submit', state = ComponentStates.ENABLED) {
39118
39118
  const sorted = [...schema].sort((a, b) => a.order - b.order);
39119
- const fields = sorted.map(def => this.buildField(def, i18nNamespace, state));
39120
- const section = { name: '', order: 0, fields };
39119
+ const sectionsById = new Map();
39120
+ sorted.forEach(def => {
39121
+ const key = def.sectionId || def.sectionLabel || '';
39122
+ const section = sectionsById.get(key) ??
39123
+ {
39124
+ name: def.sectionLabel || '',
39125
+ order: def.sectionOrder ?? 0,
39126
+ fields: [],
39127
+ };
39128
+ section.fields.push(this.buildField(def, i18nNamespace, state));
39129
+ sectionsById.set(key, section);
39130
+ });
39131
+ const sections = [...sectionsById.values()].sort((a, b) => a.order - b.order);
39121
39132
  return {
39122
39133
  name: formName,
39123
- sections: [section],
39134
+ sections,
39124
39135
  actions: button({
39125
39136
  text: this.i18n.t(submitLabelKey, i18nNamespace),
39126
39137
  type: 'submit',
@@ -39195,6 +39206,9 @@ class FormSchemaBuilderService {
39195
39206
  required: !!field.required,
39196
39207
  validators,
39197
39208
  options: this.normalizeOptions(field.options),
39209
+ sectionId: field.sectionId,
39210
+ sectionLabel: field.sectionLabel?.trim() ?? '',
39211
+ sectionOrder: field.sectionOrder,
39198
39212
  step: field.step,
39199
39213
  unitLabel: field.unitLabel,
39200
39214
  numberPickerVariant: field.numberPickerVariant,
@@ -39399,6 +39413,9 @@ const SURVEY_RESPONSE_I18N = {
39399
39413
  required: 'Esta pregunta es obligatoria',
39400
39414
  yes: 'Sí',
39401
39415
  no: 'No',
39416
+ previous: 'Volver',
39417
+ next: 'Siguiente',
39418
+ stepsLabel: 'Pasos de la encuesta',
39402
39419
  submit: 'Enviar',
39403
39420
  alreadyTitle: 'Ya respondiste esta encuesta',
39404
39421
  alreadyBody: 'Tu respuesta quedó registrada la primera vez. Gracias por tu tiempo.',
@@ -39422,6 +39439,9 @@ const SURVEY_RESPONSE_I18N = {
39422
39439
  required: 'This question is required',
39423
39440
  yes: 'Yes',
39424
39441
  no: 'No',
39442
+ previous: 'Back',
39443
+ next: 'Next',
39444
+ stepsLabel: 'Survey steps',
39425
39445
  submit: 'Submit',
39426
39446
  alreadyTitle: 'You already answered this survey',
39427
39447
  alreadyBody: 'Your response was recorded the first time. Thanks for your time.',
@@ -39479,6 +39499,8 @@ class SurveyResponseComponent {
39479
39499
  this.sending = signal(false);
39480
39500
  this.typeConfig = signal(null);
39481
39501
  this.alreadyAnswered = signal(false);
39502
+ this.responseStep = signal(0);
39503
+ this.responseValues = signal({});
39482
39504
  this.nameControl = new FormControl('', { nonNullable: true });
39483
39505
  this.emailControl = new FormControl('', { nonNullable: true });
39484
39506
  this.form = new FormGroup({});
@@ -39487,7 +39509,20 @@ class SurveyResponseComponent {
39487
39509
  if (!cfg)
39488
39510
  return null;
39489
39511
  const form = this.formSchemas.buildForm(this.surveySchemaForForm(cfg.fieldSchema), NAMESPACE$4, '', 'submit', this.sending() ? ComponentStates.WORKING : ComponentStates.ENABLED);
39490
- return { ...form, showDividers: false, fieldSpacing: 16 };
39512
+ const sections = this.hasResponseSteps() ? [this.sectionWithStoredValues(this.responseSections()[this.responseStep()])] : form.sections;
39513
+ return {
39514
+ ...form,
39515
+ sections,
39516
+ showDividers: false,
39517
+ fieldSpacing: 16,
39518
+ controlled: this.hasResponseSteps(),
39519
+ };
39520
+ });
39521
+ this.responseSections = computed(() => {
39522
+ const cfg = this.typeConfig();
39523
+ if (!cfg)
39524
+ return [];
39525
+ return this.formSchemas.buildForm(this.surveySchemaForForm(cfg.fieldSchema), NAMESPACE$4, '', 'submit').sections;
39491
39526
  });
39492
39527
  /** Preguntas ordenadas, expuestas para tests/hosts que inspeccionan el schema resuelto. */
39493
39528
  this.questions = computed(() => {
@@ -39565,6 +39600,19 @@ class SurveyResponseComponent {
39565
39600
  }
39566
39601
  this.form = new FormGroup(group);
39567
39602
  }
39603
+ hasResponseSteps() {
39604
+ return this.responseSections().filter(section => !!section.name).length > 1;
39605
+ }
39606
+ sectionWithStoredValues(section) {
39607
+ const values = this.responseValues();
39608
+ return {
39609
+ ...section,
39610
+ fields: section.fields.map(field => ({
39611
+ ...field,
39612
+ value: Object.prototype.hasOwnProperty.call(values, field.name) ? values[field.name] : field.value,
39613
+ })),
39614
+ };
39615
+ }
39568
39616
  questionControl(name) {
39569
39617
  return (this.formComponent?.Form.get(name) ?? this.form.get(name));
39570
39618
  }
@@ -39667,10 +39715,14 @@ class SurveyResponseComponent {
39667
39715
  if (!cfg)
39668
39716
  return;
39669
39717
  this.sending.set(true);
39718
+ const fields = {
39719
+ ...this.responseValues(),
39720
+ ...(event?.fields ?? this.formComponent?.Form.getRawValue() ?? this.form.getRawValue()),
39721
+ };
39670
39722
  const payload = {
39671
39723
  type: cfg.typeId,
39672
39724
  title: cfg.label,
39673
- fields: event?.fields ?? this.formComponent?.Form.getRawValue() ?? this.form.getRawValue(),
39725
+ fields,
39674
39726
  ...(this.props.inviteToken ? { inviteToken: this.props.inviteToken } : {}),
39675
39727
  ...(this.needsIdentity()
39676
39728
  ? { submitter: { name: this.nameControl.value.trim(), email: this.emailControl.value.trim() } }
@@ -39701,6 +39753,54 @@ class SurveyResponseComponent {
39701
39753
  });
39702
39754
  }
39703
39755
  }
39756
+ nextResponseStep() {
39757
+ if (!this.formComponent?.validate()) {
39758
+ this.markInvalid();
39759
+ return;
39760
+ }
39761
+ this.responseValues.update(values => ({ ...values, ...this.formComponent?.Form.getRawValue() }));
39762
+ this.responseStep.update(step => Math.min(step + 1, this.responseSections().length - 1));
39763
+ }
39764
+ previousResponseStep() {
39765
+ this.responseValues.update(values => ({ ...values, ...this.formComponent?.Form.getRawValue() }));
39766
+ this.responseStep.update(step => Math.max(step - 1, 0));
39767
+ }
39768
+ responseBackProps() {
39769
+ return {
39770
+ token: 'survey-response-back',
39771
+ text: this.t('previous'),
39772
+ color: 'dark',
39773
+ fill: 'outline',
39774
+ shape: 'round',
39775
+ size: 'default',
39776
+ type: 'button',
39777
+ state: ComponentStates.ENABLED,
39778
+ };
39779
+ }
39780
+ responseNextProps() {
39781
+ return {
39782
+ token: 'survey-response-next',
39783
+ text: this.t('next'),
39784
+ color: 'dark',
39785
+ fill: 'solid',
39786
+ shape: 'round',
39787
+ size: 'default',
39788
+ type: 'button',
39789
+ state: ComponentStates.ENABLED,
39790
+ };
39791
+ }
39792
+ responseSubmitProps() {
39793
+ return {
39794
+ token: 'survey-response-submit',
39795
+ text: this.t('submit'),
39796
+ color: 'dark',
39797
+ fill: 'solid',
39798
+ shape: 'round',
39799
+ size: 'default',
39800
+ type: 'button',
39801
+ state: this.sending() ? ComponentStates.WORKING : ComponentStates.ENABLED,
39802
+ };
39803
+ }
39704
39804
  hasMissingRequiredMultiSelect() {
39705
39805
  return this.questions()
39706
39806
  .filter(q => q.type === 'MULTI_SELECT' && q.required)
@@ -39764,13 +39864,38 @@ class SurveyResponseComponent {
39764
39864
  }
39765
39865
 
39766
39866
  @if (formProps(); as props) {
39867
+ @if (hasResponseSteps()) {
39868
+ <nav class="survey-response__steps" [attr.aria-label]="t('stepsLabel')">
39869
+ @for (section of responseSections(); track section.name; let i = $index) {
39870
+ <span
39871
+ class="survey-response__step"
39872
+ [class.survey-response__step--active]="responseStep() === i"
39873
+ [attr.aria-current]="responseStep() === i ? 'step' : null"
39874
+ >
39875
+ {{ i + 1 }}
39876
+ </span>
39877
+ }
39878
+ </nav>
39879
+ }
39767
39880
  <val-form [props]="props" (onSubmit)="submit($event)" (onInvalid)="markInvalid()" />
39881
+ @if (hasResponseSteps()) {
39882
+ <div class="survey-response__actions">
39883
+ @if (responseStep() > 0) {
39884
+ <val-button [props]="responseBackProps()" (onClick)="previousResponseStep()" />
39885
+ }
39886
+ @if (responseStep() < responseSections().length - 1) {
39887
+ <val-button [props]="responseNextProps()" (onClick)="nextResponseStep()" />
39888
+ } @else {
39889
+ <val-button [props]="responseSubmitProps()" (onClick)="submit()" />
39890
+ }
39891
+ </div>
39892
+ }
39768
39893
  }
39769
39894
  </div>
39770
39895
  }
39771
39896
  }
39772
39897
  }
39773
- `, isInline: true, styles: [":host{display:block}.survey-response{display:flex;flex-direction:column;gap:16px}.survey-response__header-image,.survey-response__success-image{display:block;width:100%;max-height:220px;object-fit:contain;border-radius:var(--val-radius-md, 12px)}.survey-response__success{align-items:center;text-align:center}.survey-response__success-image{max-width:320px}.survey-response__success-note{margin:0;color:var(--ion-color-medium, #92949c);font-size:.95rem;font-weight:600}.survey-response__error{margin:0;color:var(--ion-color-danger);font-size:.875rem;font-weight:700}.survey-response__hint{display:block;margin-top:4px;font-size:.8125rem;color:var(--ion-color-medium, #92949c)}.survey-response__loading{min-height:120px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextInputComponent, selector: "val-text-input", inputs: ["preset", "props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onValueChange", "onInvalid", "onSelectChange"] }] }); }
39898
+ `, isInline: true, styles: [":host{display:block}.survey-response{display:flex;flex-direction:column;gap:16px}.survey-response__header-image,.survey-response__success-image{display:block;width:100%;max-height:220px;object-fit:contain;border-radius:var(--val-radius-md, 12px)}.survey-response__success{align-items:center;text-align:center}.survey-response__success-image{max-width:320px}.survey-response__success-note{margin:0;color:var(--ion-color-medium, #92949c);font-size:.95rem;font-weight:600}.survey-response__steps{display:flex;gap:6px}.survey-response__step{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .12));border-radius:999px;color:var(--ion-color-medium, #92949c);font-size:.8125rem;font-weight:700}.survey-response__step--active{border-color:var(--ion-color-primary);color:var(--ion-color-primary)}.survey-response__actions{display:flex;justify-content:flex-end;gap:8px}.survey-response__error{margin:0;color:var(--ion-color-danger);font-size:.875rem;font-weight:700}.survey-response__hint{display:block;margin-top:4px;font-size:.8125rem;color:var(--ion-color-medium, #92949c)}.survey-response__loading{min-height:120px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextInputComponent, selector: "val-text-input", inputs: ["preset", "props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onValueChange", "onInvalid", "onSelectChange"] }] }); }
39774
39899
  }
39775
39900
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SurveyResponseComponent, decorators: [{
39776
39901
  type: Component,
@@ -39841,13 +39966,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
39841
39966
  }
39842
39967
 
39843
39968
  @if (formProps(); as props) {
39969
+ @if (hasResponseSteps()) {
39970
+ <nav class="survey-response__steps" [attr.aria-label]="t('stepsLabel')">
39971
+ @for (section of responseSections(); track section.name; let i = $index) {
39972
+ <span
39973
+ class="survey-response__step"
39974
+ [class.survey-response__step--active]="responseStep() === i"
39975
+ [attr.aria-current]="responseStep() === i ? 'step' : null"
39976
+ >
39977
+ {{ i + 1 }}
39978
+ </span>
39979
+ }
39980
+ </nav>
39981
+ }
39844
39982
  <val-form [props]="props" (onSubmit)="submit($event)" (onInvalid)="markInvalid()" />
39983
+ @if (hasResponseSteps()) {
39984
+ <div class="survey-response__actions">
39985
+ @if (responseStep() > 0) {
39986
+ <val-button [props]="responseBackProps()" (onClick)="previousResponseStep()" />
39987
+ }
39988
+ @if (responseStep() < responseSections().length - 1) {
39989
+ <val-button [props]="responseNextProps()" (onClick)="nextResponseStep()" />
39990
+ } @else {
39991
+ <val-button [props]="responseSubmitProps()" (onClick)="submit()" />
39992
+ }
39993
+ </div>
39994
+ }
39845
39995
  }
39846
39996
  </div>
39847
39997
  }
39848
39998
  }
39849
39999
  }
39850
- `, styles: [":host{display:block}.survey-response{display:flex;flex-direction:column;gap:16px}.survey-response__header-image,.survey-response__success-image{display:block;width:100%;max-height:220px;object-fit:contain;border-radius:var(--val-radius-md, 12px)}.survey-response__success{align-items:center;text-align:center}.survey-response__success-image{max-width:320px}.survey-response__success-note{margin:0;color:var(--ion-color-medium, #92949c);font-size:.95rem;font-weight:600}.survey-response__error{margin:0;color:var(--ion-color-danger);font-size:.875rem;font-weight:700}.survey-response__hint{display:block;margin-top:4px;font-size:.8125rem;color:var(--ion-color-medium, #92949c)}.survey-response__loading{min-height:120px}\n"] }]
40000
+ `, styles: [":host{display:block}.survey-response{display:flex;flex-direction:column;gap:16px}.survey-response__header-image,.survey-response__success-image{display:block;width:100%;max-height:220px;object-fit:contain;border-radius:var(--val-radius-md, 12px)}.survey-response__success{align-items:center;text-align:center}.survey-response__success-image{max-width:320px}.survey-response__success-note{margin:0;color:var(--ion-color-medium, #92949c);font-size:.95rem;font-weight:600}.survey-response__steps{display:flex;gap:6px}.survey-response__step{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .12));border-radius:999px;color:var(--ion-color-medium, #92949c);font-size:.8125rem;font-weight:700}.survey-response__step--active{border-color:var(--ion-color-primary);color:var(--ion-color-primary)}.survey-response__actions{display:flex;justify-content:flex-end;gap:8px}.survey-response__error{margin:0;color:var(--ion-color-danger);font-size:.875rem;font-weight:700}.survey-response__hint{display:block;margin-top:4px;font-size:.8125rem;color:var(--ion-color-medium, #92949c)}.survey-response__loading{min-height:120px}\n"] }]
39851
40001
  }], ctorParameters: () => [], propDecorators: { props: [{
39852
40002
  type: Input
39853
40003
  }], submitted: [{
@@ -39865,6 +40015,8 @@ const FIELD_SCHEMA_EDITOR_I18N = {
39865
40015
  typeLabel: 'Tipo de campo',
39866
40016
  placeholderLabel: 'Texto de ayuda dentro del campo',
39867
40017
  hintLabel: 'Nota bajo el campo',
40018
+ sectionLabel: 'Sección',
40019
+ sectionPlaceholder: 'Ej: Información personal',
39868
40020
  requiredLabel: 'Obligatorio',
39869
40021
  optionsTitle: 'Opciones',
39870
40022
  optionsHint: 'Estas son las alternativas que puede elegir quien responde.',
@@ -39900,6 +40052,8 @@ const FIELD_SCHEMA_EDITOR_I18N = {
39900
40052
  typeLabel: 'Field type',
39901
40053
  placeholderLabel: 'Helper text inside the field',
39902
40054
  hintLabel: 'Note below the field',
40055
+ sectionLabel: 'Section',
40056
+ sectionPlaceholder: 'E.g: Personal information',
39903
40057
  requiredLabel: 'Required',
39904
40058
  optionsTitle: 'Options',
39905
40059
  optionsHint: 'These are the choices the person can pick.',
@@ -39974,6 +40128,7 @@ class FieldSchemaEditorComponent {
39974
40128
  this.labelControl = new FormControl('', { nonNullable: true });
39975
40129
  this.placeholderControl = new FormControl('', { nonNullable: true });
39976
40130
  this.hintControl = new FormControl('', { nonNullable: true });
40131
+ this.sectionControl = new FormControl('', { nonNullable: true });
39977
40132
  this.requiredControl = new FormControl(true, { nonNullable: true });
39978
40133
  this.typeControl = new FormControl('TEXT', { nonNullable: true });
39979
40134
  this.options = signal([]);
@@ -40029,6 +40184,7 @@ class FieldSchemaEditorComponent {
40029
40184
  });
40030
40185
  this.placeholderControl.valueChanges.subscribe(() => this.emitChange());
40031
40186
  this.hintControl.valueChanges.subscribe(() => this.emitChange());
40187
+ this.sectionControl.valueChanges.subscribe(() => this.emitChange());
40032
40188
  this.requiredControl.valueChanges.subscribe(() => this.emitChange());
40033
40189
  }
40034
40190
  ngOnInit() {
@@ -40040,6 +40196,7 @@ class FieldSchemaEditorComponent {
40040
40196
  this.labelControl.setValue(initial.label ?? '');
40041
40197
  this.placeholderControl.setValue(initial.placeholderText ?? '');
40042
40198
  this.hintControl.setValue(initial.hintText ?? '');
40199
+ this.sectionControl.setValue(initial.sectionLabel ?? '');
40043
40200
  this.requiredControl.setValue(!!initial.required);
40044
40201
  this.typeControl.setValue(this.coerceType(initial.type));
40045
40202
  this.options.set(initial.options ?? []);
@@ -40087,6 +40244,7 @@ class FieldSchemaEditorComponent {
40087
40244
  this.labelControl.setValue(value.label ?? '');
40088
40245
  this.placeholderControl.setValue(value.placeholderText ?? '');
40089
40246
  this.hintControl.setValue(value.hintText ?? '');
40247
+ this.sectionControl.setValue(value.sectionLabel ?? '');
40090
40248
  this.requiredControl.setValue(!!value.required);
40091
40249
  this.typeControl.setValue(this.coerceType(value.type));
40092
40250
  this.options.set(value.options ?? []);
@@ -40121,6 +40279,7 @@ class FieldSchemaEditorComponent {
40121
40279
  hintText: (this.hintControl.value || '').trim(),
40122
40280
  type: this.coerceType(this.typeControl.value),
40123
40281
  required: this.requiredControl.value,
40282
+ sectionLabel: (this.sectionControl.value || '').trim(),
40124
40283
  options: this.needsOptions() ? this.options().map((option, index) => ({ ...option, order: index })) : undefined,
40125
40284
  };
40126
40285
  }
@@ -40181,6 +40340,10 @@ class FieldSchemaEditorComponent {
40181
40340
  </val-form-field>
40182
40341
  }
40183
40342
 
40343
+ <val-form-field [label]="t('sectionLabel')">
40344
+ <val-text-input [props]="{ control: sectionControl, placeholder: t('sectionPlaceholder'), state: state() }" />
40345
+ </val-form-field>
40346
+
40184
40347
  <val-toggle-input
40185
40348
  [props]="{
40186
40349
  control: requiredControl,
@@ -40253,6 +40416,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
40253
40416
  </val-form-field>
40254
40417
  }
40255
40418
 
40419
+ <val-form-field [label]="t('sectionLabel')">
40420
+ <val-text-input [props]="{ control: sectionControl, placeholder: t('sectionPlaceholder'), state: state() }" />
40421
+ </val-form-field>
40422
+
40256
40423
  <val-toggle-input
40257
40424
  [props]="{
40258
40425
  control: requiredControl,
@@ -40746,8 +40913,12 @@ const SURVEY_BUILDER_I18N = {
40746
40913
  wizardStepsLabel: 'Pasos para crear la encuesta',
40747
40914
  next: 'Siguiente',
40748
40915
  previous: 'Volver',
40916
+ preview: 'Vista previa',
40917
+ previewTitle: 'Vista previa',
40918
+ closePreview: 'Cerrar',
40749
40919
  reviewTitle: 'Resumen',
40750
40920
  reviewNoSubtitle: 'Sin subtítulo',
40921
+ reviewQuestionCount: '{count} pregunta',
40751
40922
  reviewQuestionsCount: '{count} preguntas',
40752
40923
  reviewHeaderImage: 'Imagen inicial configurada',
40753
40924
  reviewSuccessImage: 'Imagen de cierre configurada',
@@ -40801,8 +40972,12 @@ const SURVEY_BUILDER_I18N = {
40801
40972
  wizardStepsLabel: 'Survey builder steps',
40802
40973
  next: 'Next',
40803
40974
  previous: 'Back',
40975
+ preview: 'Preview',
40976
+ previewTitle: 'Preview',
40977
+ closePreview: 'Close',
40804
40978
  reviewTitle: 'Summary',
40805
40979
  reviewNoSubtitle: 'No subtitle',
40980
+ reviewQuestionCount: '{count} question',
40806
40981
  reviewQuestionsCount: '{count} questions',
40807
40982
  reviewHeaderImage: 'Opening image set',
40808
40983
  reviewSuccessImage: 'Closing image set',
@@ -40810,6 +40985,129 @@ const SURVEY_BUILDER_I18N = {
40810
40985
  },
40811
40986
  };
40812
40987
 
40988
+ class SurveyPreviewModalComponent {
40989
+ constructor() {
40990
+ this.modalTitle = '';
40991
+ this.closeLabel = '';
40992
+ this.title = '';
40993
+ this.subtitle = '';
40994
+ this.questions = [];
40995
+ this.successTitle = '';
40996
+ this.successMessage = '';
40997
+ this.requiredSuffix = '';
40998
+ }
40999
+ sections() {
41000
+ const map = new Map();
41001
+ [...this.questions]
41002
+ .sort((a, b) => a.order - b.order)
41003
+ .forEach(question => {
41004
+ const key = question.sectionId || question.sectionLabel || '';
41005
+ const section = map.get(key) ??
41006
+ {
41007
+ name: question.sectionLabel || '',
41008
+ order: question.sectionOrder ?? 0,
41009
+ questions: [],
41010
+ };
41011
+ section.questions.push(question);
41012
+ map.set(key, section);
41013
+ });
41014
+ return [...map.values()].sort((a, b) => a.order - b.order);
41015
+ }
41016
+ close() {
41017
+ this._modalRef?.dismiss(undefined, 'close');
41018
+ }
41019
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SurveyPreviewModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
41020
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SurveyPreviewModalComponent, isStandalone: true, selector: "val-survey-preview-modal", inputs: { modalTitle: "modalTitle", closeLabel: "closeLabel", title: "title", subtitle: "subtitle", presentation: "presentation", questions: "questions", successTitle: "successTitle", successMessage: "successMessage", requiredSuffix: "requiredSuffix", _modalRef: "_modalRef" }, ngImport: i0, template: `
41021
+ <val-modal-layout [title]="modalTitle" [closeLabel]="closeLabel" (close)="close()">
41022
+ <div class="survey-preview">
41023
+ @if (presentation?.headerImageUrl) {
41024
+ <img class="survey-preview__image" [src]="presentation?.headerImageUrl" [alt]="title" />
41025
+ }
41026
+ <val-display [props]="{ content: title, size: 'small', color: 'dark' }" />
41027
+ @if (subtitle) {
41028
+ <val-title [props]="{ content: subtitle, size: 'large', color: '', bold: false }" />
41029
+ }
41030
+ @for (section of sections(); track section.name) {
41031
+ <section class="survey-preview__section">
41032
+ @if (section.name) {
41033
+ <strong class="survey-preview__section-title">{{ section.name }}</strong>
41034
+ }
41035
+ @for (question of section.questions; track question.name) {
41036
+ <article class="survey-preview__question">
41037
+ <strong>{{ question.label }}</strong>
41038
+ <span>{{ question.typeLabel }}{{ question.required ? requiredSuffix : '' }}</span>
41039
+ </article>
41040
+ }
41041
+ </section>
41042
+ }
41043
+ <section class="survey-preview__success">
41044
+ @if (presentation?.successImageUrl) {
41045
+ <img class="survey-preview__success-image" [src]="presentation?.successImageUrl" [alt]="successTitle" />
41046
+ }
41047
+ <strong>{{ successTitle }}</strong>
41048
+ <span>{{ successMessage }}</span>
41049
+ </section>
41050
+ </div>
41051
+ </val-modal-layout>
41052
+ `, isInline: true, styles: [":host{display:block}.survey-preview{display:flex;flex-direction:column;gap:16px}.survey-preview__image,.survey-preview__success-image{display:block;width:100%;max-height:220px;object-fit:contain;border-radius:var(--val-radius-md, 12px)}.survey-preview__section{display:flex;flex-direction:column;gap:10px}.survey-preview__section-title{color:var(--ion-text-color, #000);font-size:1rem}.survey-preview__question,.survey-preview__success{display:flex;flex-direction:column;gap:4px;padding:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));border-radius:var(--val-radius-md, 12px);background:var(--ion-card-background, var(--ion-background-color, #fff))}.survey-preview__question span,.survey-preview__success span{color:var(--ion-color-medium, #92949c);font-size:.875rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: ModalLayoutComponent, selector: "val-modal-layout", inputs: ["title", "subtitle", "closeLabel", "showClose", "actions", "actionsAlign", "footer", "footerClass"], outputs: ["close", "actionClick"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }] }); }
41053
+ }
41054
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SurveyPreviewModalComponent, decorators: [{
41055
+ type: Component,
41056
+ args: [{ selector: 'val-survey-preview-modal', standalone: true, imports: [CommonModule, ModalLayoutComponent, DisplayComponent, TitleComponent], template: `
41057
+ <val-modal-layout [title]="modalTitle" [closeLabel]="closeLabel" (close)="close()">
41058
+ <div class="survey-preview">
41059
+ @if (presentation?.headerImageUrl) {
41060
+ <img class="survey-preview__image" [src]="presentation?.headerImageUrl" [alt]="title" />
41061
+ }
41062
+ <val-display [props]="{ content: title, size: 'small', color: 'dark' }" />
41063
+ @if (subtitle) {
41064
+ <val-title [props]="{ content: subtitle, size: 'large', color: '', bold: false }" />
41065
+ }
41066
+ @for (section of sections(); track section.name) {
41067
+ <section class="survey-preview__section">
41068
+ @if (section.name) {
41069
+ <strong class="survey-preview__section-title">{{ section.name }}</strong>
41070
+ }
41071
+ @for (question of section.questions; track question.name) {
41072
+ <article class="survey-preview__question">
41073
+ <strong>{{ question.label }}</strong>
41074
+ <span>{{ question.typeLabel }}{{ question.required ? requiredSuffix : '' }}</span>
41075
+ </article>
41076
+ }
41077
+ </section>
41078
+ }
41079
+ <section class="survey-preview__success">
41080
+ @if (presentation?.successImageUrl) {
41081
+ <img class="survey-preview__success-image" [src]="presentation?.successImageUrl" [alt]="successTitle" />
41082
+ }
41083
+ <strong>{{ successTitle }}</strong>
41084
+ <span>{{ successMessage }}</span>
41085
+ </section>
41086
+ </div>
41087
+ </val-modal-layout>
41088
+ `, styles: [":host{display:block}.survey-preview{display:flex;flex-direction:column;gap:16px}.survey-preview__image,.survey-preview__success-image{display:block;width:100%;max-height:220px;object-fit:contain;border-radius:var(--val-radius-md, 12px)}.survey-preview__section{display:flex;flex-direction:column;gap:10px}.survey-preview__section-title{color:var(--ion-text-color, #000);font-size:1rem}.survey-preview__question,.survey-preview__success{display:flex;flex-direction:column;gap:4px;padding:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));border-radius:var(--val-radius-md, 12px);background:var(--ion-card-background, var(--ion-background-color, #fff))}.survey-preview__question span,.survey-preview__success span{color:var(--ion-color-medium, #92949c);font-size:.875rem}\n"] }]
41089
+ }], propDecorators: { modalTitle: [{
41090
+ type: Input
41091
+ }], closeLabel: [{
41092
+ type: Input
41093
+ }], title: [{
41094
+ type: Input
41095
+ }], subtitle: [{
41096
+ type: Input
41097
+ }], presentation: [{
41098
+ type: Input
41099
+ }], questions: [{
41100
+ type: Input
41101
+ }], successTitle: [{
41102
+ type: Input
41103
+ }], successMessage: [{
41104
+ type: Input
41105
+ }], requiredSuffix: [{
41106
+ type: Input
41107
+ }], _modalRef: [{
41108
+ type: Input
41109
+ }] } });
41110
+
40813
41111
  const NAMESPACE$1 = 'SurveyBuilder';
40814
41112
  const EDITOR_NAMESPACE = 'FieldSchemaEditor';
40815
41113
  const DEFAULT_TYPE_PREFIX = 'survey';
@@ -40846,6 +41144,7 @@ class SurveyBuilderComponent {
40846
41144
  this.requests = inject(RequestService);
40847
41145
  this.formBuilder = inject(RequestFormBuilderService);
40848
41146
  this.errors = inject(ValtechErrorService);
41147
+ this.modals = inject(ModalService);
40849
41148
  this.titleControl = new FormControl('', { nonNullable: true });
40850
41149
  this.subtitleControl = new FormControl('', { nonNullable: true });
40851
41150
  this.headerImageControl = new FormControl('', { nonNullable: true });
@@ -40877,7 +41176,7 @@ class SurveyBuilderComponent {
40877
41176
  }
40878
41177
  this.successTitleControl.setValue(this.defaultSuccessTitle());
40879
41178
  this.successMessageControl.setValue(this.defaultSuccessMessage());
40880
- addIcons({ cloudUploadOutline, trashOutline });
41179
+ addIcons({ cloudUploadOutline, eyeOutline, trashOutline });
40881
41180
  }
40882
41181
  async ngOnInit() {
40883
41182
  // Sembrado directo: la app ya tenía la encuesta, no hace falta leerla.
@@ -41126,6 +41425,9 @@ class SurveyBuilderComponent {
41126
41425
  label: row.label.trim(),
41127
41426
  type: row.type,
41128
41427
  required: row.required,
41428
+ sectionId: row.sectionLabel ? this.formBuilder.toToken(row.sectionLabel) : undefined,
41429
+ sectionLabel: row.sectionLabel,
41430
+ sectionOrder: this.sectionOrderFor(row.sectionLabel, rows),
41129
41431
  placeholderText: row.placeholderText,
41130
41432
  hintText: row.hintText,
41131
41433
  options: row.options,
@@ -41235,9 +41537,60 @@ class SurveyBuilderComponent {
41235
41537
  hintText: f.hintText ?? '',
41236
41538
  type: f.type,
41237
41539
  required: !!f.required,
41540
+ sectionLabel: f.sectionLabel ?? '',
41541
+ sectionOrder: f.sectionOrder,
41238
41542
  options: f.options,
41239
41543
  };
41240
41544
  }
41545
+ reviewQuestionsCount() {
41546
+ const count = this.questions().length;
41547
+ const key = count === 1 ? 'reviewQuestionCount' : 'reviewQuestionsCount';
41548
+ return this.interpolate(this.t(key), { count });
41549
+ }
41550
+ previewProps() {
41551
+ return {
41552
+ token: 'survey-builder-preview',
41553
+ text: this.t('preview'),
41554
+ color: 'dark',
41555
+ fill: 'outline',
41556
+ shape: 'round',
41557
+ size: 'default',
41558
+ type: 'button',
41559
+ state: ComponentStates.ENABLED,
41560
+ icon: { name: 'eye-outline', slot: 'start' },
41561
+ };
41562
+ }
41563
+ async openPreview() {
41564
+ await this.modals.open({
41565
+ component: SurveyPreviewModalComponent,
41566
+ componentProps: {
41567
+ modalTitle: this.t('previewTitle'),
41568
+ closeLabel: this.t('closePreview'),
41569
+ title: this.titleControl.value.trim() || this.t('untitledQuestion'),
41570
+ subtitle: this.subtitleControl.value.trim(),
41571
+ presentation: this.presentationPayload(),
41572
+ successTitle: this.successTitleControl.value.trim() || this.defaultSuccessTitle(),
41573
+ successMessage: this.successMessageControl.value.trim() || this.defaultSuccessMessage(),
41574
+ requiredSuffix: ` · ${this.t('requiredTag')}`,
41575
+ questions: this.questions().map((question, index) => ({
41576
+ ...question,
41577
+ order: index,
41578
+ name: question.name || this.formBuilder.toToken(question.label),
41579
+ typeLabel: this.typeLabel(question.type),
41580
+ sectionId: question.sectionLabel ? this.formBuilder.toToken(question.sectionLabel) : undefined,
41581
+ sectionOrder: this.sectionOrderFor(question.sectionLabel, this.questions()),
41582
+ })),
41583
+ },
41584
+ size: 'medium',
41585
+ });
41586
+ }
41587
+ sectionOrderFor(sectionLabel, rows) {
41588
+ const label = sectionLabel?.trim();
41589
+ if (!label)
41590
+ return undefined;
41591
+ const labels = [...new Set(rows.map(row => row.sectionLabel?.trim()).filter(Boolean))];
41592
+ return labels.indexOf(label);
41593
+ }
41241
41594
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SurveyBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
41242
41595
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SurveyBuilderComponent, isStandalone: true, selector: "val-survey-builder", inputs: { props: "props" }, outputs: { saved: "saved", draft: "draft" }, ngImport: i0, template: `
41243
41596
  <div class="survey-builder">
@@ -41454,7 +41807,9 @@ class SurveyBuilderComponent {
41454
41807
  <div class="survey-builder__row-head">
41455
41808
  <div class="survey-builder__row-text">
41456
41809
  <strong>{{ row.label || t('untitledQuestion') }}</strong>
41457
- <span>{{ typeLabel(row.type) }}{{ row.required ? ' · ' + t('requiredTag') : '' }}</span>
41810
+ <span>
41811
+ {{ typeLabel(row.type) }}{{ row.required ? ' · ' + t('requiredTag') : '' }}{{ row.sectionLabel ? ' · ' + row.sectionLabel : '' }}
41812
+ </span>
41458
41813
  </div>
41459
41814
  <div class="survey-builder__row-actions">
41460
41815
  <val-button
@@ -41528,11 +41883,12 @@ class SurveyBuilderComponent {
41528
41883
  <span class="survey-builder__questions-label">{{ t('reviewTitle') }}</span>
41529
41884
  <strong>{{ titleControl.value || t('untitledQuestion') }}</strong>
41530
41885
  <span>{{ subtitleControl.value || t('reviewNoSubtitle') }}</span>
41531
- <span>{{ interpolate(t('reviewQuestionsCount'), { count: questions().length }) }}</span>
41886
+ <span>{{ reviewQuestionsCount() }}</span>
41532
41887
  @if (headerImageControl.value) {
41533
41888
  <span>{{ t('reviewHeaderImage') }}</span>
41534
41889
  }
41535
41890
  <span>{{ successImageControl.value ? t('reviewSuccessImage') : t('reviewDefaultClose') }}</span>
41891
+ <val-button [props]="previewProps()" (onClick)="openPreview()" />
41536
41892
  </section>
41537
41893
  }
41538
41894
 
@@ -41783,7 +42139,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
41783
42139
  <div class="survey-builder__row-head">
41784
42140
  <div class="survey-builder__row-text">
41785
42141
  <strong>{{ row.label || t('untitledQuestion') }}</strong>
41786
- <span>{{ typeLabel(row.type) }}{{ row.required ? ' · ' + t('requiredTag') : '' }}</span>
42142
+ <span>
42143
+ {{ typeLabel(row.type) }}{{ row.required ? ' · ' + t('requiredTag') : '' }}{{ row.sectionLabel ? ' · ' + row.sectionLabel : '' }}
42144
+ </span>
41787
42145
  </div>
41788
42146
  <div class="survey-builder__row-actions">
41789
42147
  <val-button
@@ -41857,11 +42215,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
41857
42215
  <span class="survey-builder__questions-label">{{ t('reviewTitle') }}</span>
41858
42216
  <strong>{{ titleControl.value || t('untitledQuestion') }}</strong>
41859
42217
  <span>{{ subtitleControl.value || t('reviewNoSubtitle') }}</span>
41860
- <span>{{ interpolate(t('reviewQuestionsCount'), { count: questions().length }) }}</span>
42218
+ <span>{{ reviewQuestionsCount() }}</span>
41861
42219
  @if (headerImageControl.value) {
41862
42220
  <span>{{ t('reviewHeaderImage') }}</span>
41863
42221
  }
41864
42222
  <span>{{ successImageControl.value ? t('reviewSuccessImage') : t('reviewDefaultClose') }}</span>
42223
+ <val-button [props]="previewProps()" (onClick)="openPreview()" />
41865
42224
  </section>
41866
42225
  }
41867
42226