@arquimedes.co/eureka-forms 3.0.55 → 3.0.56-test

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.
@@ -9,6 +9,13 @@ export interface GBaseStep {
9
9
  clientInfoType?: string;
10
10
  dependencies?: string[];
11
11
  condition?: Condition;
12
+ /**
13
+ * Per-screen column-span overrides (tablet/mobile) written by the form editor. Desktop's
14
+ * source of truth stays the step's legacy `size`, and a missing entry falls back to the span
15
+ * derived from it (see `resolveStepSpan` in FormSteps/StepFunctions), so a form with no
16
+ * explicit overrides renders exactly like the legacy shape.
17
+ */
18
+ sizes?: Partial<Record<'tablet' | 'mobile', 1 | 2 | 3 | 4>>;
12
19
  }
13
20
  export interface GTitle extends GBaseStep {
14
21
  title: string;
@@ -0,0 +1,123 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import '@testing-library/jest-dom';
6
+ import { Provider } from 'react-redux';
7
+ import { useForm, FormProvider } from 'react-hook-form';
8
+ import { configureStore } from '@reduxjs/toolkit';
9
+ import { EurekaFormsReducer, defaultRootState } from '../../Utils/store';
10
+ import { RootApi } from '../../Utils/_api';
11
+ import FormStepTypes from '../../constants/FormStepTypes';
12
+ import { StoreContext } from '../../Utils/StoreContext';
13
+ import { IdFormContext } from '../../Contexts/FormContext';
14
+ import FormContext from '../../Contexts/FormContext';
15
+ import SectionContext from '../../Contexts/SectionContext';
16
+ import MaterialProviders from '../../Utils/MaterialProviders';
17
+ import InternalFormStyle from '../../constants/InternalFormStyle';
18
+ import CheckBoxStep from './CheckBoxStep';
19
+ import styles from './MaterialCheckBoxStep/MaterialCheckBoxStep.module.css';
20
+ const STORY_FORM_ID = 'STORY_FORM';
21
+ const LONG_LABEL = 'Cumplimiento de lo ofrecido frente a lo entregado en su vivienda';
22
+ const checkBoxStep = {
23
+ id: 'checkbox-step',
24
+ idSection: 'SECTION_1',
25
+ stepPath: ['checkbox-step'],
26
+ type: FormStepTypes.CHECKBOX,
27
+ label: LONG_LABEL,
28
+ description: null,
29
+ required: false,
30
+ size: 2,
31
+ };
32
+ const form = {
33
+ firstSection: 'SECTION_1',
34
+ sections: {
35
+ SECTION_1: { id: 'SECTION_1', name: 'Sección 1', steps: [], nextSection: null },
36
+ },
37
+ steps: { 'checkbox-step': { ...checkBoxStep } },
38
+ confirmationMessage: { blocks: [], entityMap: {} },
39
+ showLink: false,
40
+ size: { blockSize: 200, blockNum: 4, spacingSize: 10 },
41
+ };
42
+ function makeStore(currentBreakPoint) {
43
+ return configureStore({
44
+ reducer: { forms: EurekaFormsReducer, [RootApi.reducerPath]: RootApi.reducer },
45
+ preloadedState: {
46
+ forms: {
47
+ [STORY_FORM_ID]: {
48
+ ...defaultRootState,
49
+ widthStats: {
50
+ currentBreakPoint,
51
+ isMobile: currentBreakPoint <= checkBoxStep.size,
52
+ isResponsive: true,
53
+ },
54
+ },
55
+ },
56
+ },
57
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(RootApi.middleware),
58
+ });
59
+ }
60
+ function Harness({ currentBreakPoint }) {
61
+ const methods = useForm({ mode: 'onTouched' });
62
+ return (_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(Provider, { store: makeStore(currentBreakPoint), context: StoreContext, children: _jsx(IdFormContext.Provider, { value: STORY_FORM_ID, children: _jsx(FormContext.Provider, { value: form, children: _jsx(SectionContext.Provider, { value: "SECTION_1", children: _jsx(FormProvider, { ...methods, children: _jsx(CheckBoxStep, { step: checkBoxStep, editable: true }) }) }) }) }) }) }));
63
+ }
64
+ function getLabelDiv() {
65
+ // the leaf div holding only the label text (the wrapping column also matches by textContent)
66
+ return screen.getByText((_, element) => element?.tagName === 'DIV' &&
67
+ element.children.length === 0 &&
68
+ element.textContent === `${LONG_LABEL}:`);
69
+ }
70
+ /** Makes every element report a 2-line-clamped overflow (jsdom has no layout). */
71
+ function mockLabelOverflow() {
72
+ Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
73
+ configurable: true,
74
+ get() {
75
+ return 100;
76
+ },
77
+ });
78
+ Object.defineProperty(HTMLElement.prototype, 'clientHeight', {
79
+ configurable: true,
80
+ get() {
81
+ return 48;
82
+ },
83
+ });
84
+ }
85
+ afterEach(() => {
86
+ // remove the overflow mocks so other tests see jsdom defaults again
87
+ delete HTMLElement.prototype.scrollHeight;
88
+ delete HTMLElement.prototype.clientHeight;
89
+ });
90
+ describe('CheckBoxStep label on collapsed (mobile) breakpoints', () => {
91
+ it('wraps the label (2-line clamp) instead of single-line truncating it when the step collapses to 100% width', () => {
92
+ // breakpoint 1 <= size 2 → MaterialInputContainer renders the step at width: 100%
93
+ render(_jsx(Harness, { currentBreakPoint: 1 }));
94
+ const label = getLabelDiv();
95
+ expect(label.className).toBe(styles.clampLbl);
96
+ expect(label.className).not.toBe(styles.overflowLbl);
97
+ // the fixed desktop pixel maxWidth must not constrain the collapsed label
98
+ expect(label.style.maxWidth).toBe('');
99
+ });
100
+ it('keeps the single-line ellipsis label on desktop widths (control)', () => {
101
+ // breakpoint 4 > size 2 → the step keeps its fixed desktop width
102
+ render(_jsx(Harness, { currentBreakPoint: 4 }));
103
+ const label = getLabelDiv();
104
+ expect(label.className).toBe(styles.overflowLbl);
105
+ expect(label.style.maxWidth).not.toBe('');
106
+ expect(screen.queryByRole('button', { name: 'ver más' })).not.toBeInTheDocument();
107
+ });
108
+ it('shows no "ver más" toggle when the label fits in the clamp', () => {
109
+ // jsdom defaults: scrollHeight === clientHeight === 0 → nothing is clipped
110
+ render(_jsx(Harness, { currentBreakPoint: 1 }));
111
+ expect(screen.queryByRole('button', { name: 'ver más' })).not.toBeInTheDocument();
112
+ });
113
+ it('offers "ver más" when the clamp clips the label, and expands/collapses on click', async () => {
114
+ mockLabelOverflow();
115
+ render(_jsx(Harness, { currentBreakPoint: 1 }));
116
+ const toggle = await screen.findByRole('button', { name: 'ver más' });
117
+ await userEvent.click(toggle);
118
+ expect(getLabelDiv().className).toBe(styles.expandedLbl);
119
+ const collapseToggle = screen.getByRole('button', { name: 'ver menos' });
120
+ await userEvent.click(collapseToggle);
121
+ expect(getLabelDiv().className).toBe(styles.clampLbl);
122
+ });
123
+ });
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import styles from './MaterialCheckBoxStep.module.css';
3
3
  import ErkCheckBox from '../../../Shared/ErkCheckbox/ErkCheckBox';
4
- import React, { useContext } from 'react';
4
+ import React, { useContext, useLayoutEffect, useRef, useState } from 'react';
5
5
  import StepComponent from '../../Step';
6
6
  import FormContext from '../../../Contexts/FormContext';
7
- import { useAppSelector } from '../../../hooks';
7
+ import { useAppSelector, selectWidthStats } from '../../../hooks';
8
8
  import { useFormStep } from '../../StepHooks';
9
9
  import StepFillerContainer from '../../Utils/@StepFiller/StepFiller';
10
10
  import MaterialInputContainer from '../../Utils/MaterialInputContainer/MaterialInputContainer';
11
- import { calcStepWidth } from '../../StepFunctions';
11
+ import { calcStepWidth, resolveStepSpan } from '../../StepFunctions';
12
12
  function CheckBoxStep({ step, editable }) {
13
13
  const { ref, value, onChange, error, field } = useFormStep(step, {
14
14
  defaultValue: step.defaultValue ?? false,
@@ -19,6 +19,26 @@ function CheckBoxStep({ step, editable }) {
19
19
  });
20
20
  const { formStyle, postview } = useAppSelector((state) => state.global);
21
21
  const form = useContext(FormContext);
22
+ const widthStats = useAppSelector(selectWidthStats);
23
+ const span = resolveStepSpan(step, widthStats, form.size);
24
+ // when the breakpoint collapses the step to width: 100% (see MaterialInputContainer),
25
+ // the fixed desktop pixel width no longer matches the real available space
26
+ const collapsed = widthStats.currentBreakPoint <= span;
27
+ const labelRef = useRef(null);
28
+ const [expanded, setExpanded] = useState(false);
29
+ const [clamped, setClamped] = useState(false);
30
+ useLayoutEffect(() => {
31
+ if (!collapsed || expanded)
32
+ return;
33
+ const measure = () => {
34
+ const label = labelRef.current;
35
+ if (label)
36
+ setClamped(label.scrollHeight > label.clientHeight);
37
+ };
38
+ measure();
39
+ window.addEventListener('resize', measure);
40
+ return () => window.removeEventListener('resize', measure);
41
+ }, [collapsed, expanded, step.label]);
22
42
  const renderNested = () => {
23
43
  const steps = value ? step.steps : step.uncheckedSteps;
24
44
  return (_jsx(React.Fragment, { children: steps?.map((idStep) => {
@@ -27,12 +47,19 @@ function CheckBoxStep({ step, editable }) {
27
47
  }) }));
28
48
  };
29
49
  const renderLabel = (colon = false) => {
30
- return (_jsxs("div", { className: step.size === 4 ? styles.checkboxLbl : styles.overflowLbl, style: {
31
- maxWidth: calcStepWidth(step.size, form.size) - 45,
32
- }, children: [step.label, step.required ? ' *' : '', colon ? ':' : ''] }));
50
+ const label = `${step.label}${step.required ? ' *' : ''}${colon ? ':' : ''}`;
51
+ if (!collapsed) {
52
+ return (_jsx("div", { className: span === 4 ? styles.checkboxLbl : styles.overflowLbl, style: {
53
+ maxWidth: calcStepWidth(span, form.size) - 45,
54
+ }, children: label }));
55
+ }
56
+ return (_jsxs("div", { className: styles.labelColumn, children: [_jsx("div", { ref: labelRef, className: expanded ? styles.expandedLbl : styles.clampLbl, children: label }), (clamped || expanded) && (_jsx("button", { type: "button", className: styles.toggleLbl, style: { color: formStyle.primaryColor }, onClick: (event) => {
57
+ event.stopPropagation();
58
+ setExpanded(!expanded);
59
+ }, children: expanded ? 'ver menos' : 'ver más' }))] }));
33
60
  };
34
61
  return (_jsxs(StepFillerContainer, { step: step, children: [_jsx(MaterialInputContainer, { step: step, editable: editable, maxHeight: step.size < 4, children: _jsxs("div", { className: styles.container, children: [_jsxs("div", { className: styles.labelCheckBoxContainer, style: {
35
- height: step.size < 4 ? 31 : undefined,
62
+ height: span < 4 && !collapsed ? 31 : undefined,
36
63
  minHeight: 31,
37
64
  alignItems: step.isList ? 'start' : 'center',
38
65
  }, children: [step.label && !step.isList && renderLabel(true), _jsx(ErkCheckBox, { ...field, padding: "0px", inputRef: ref, error: !!error, checked: value, onChange: onChange, "data-testid": step.id, readOnly: !editable || postview }), step.label && step.isList && renderLabel()] }), (step.description ?? !!error) && (_jsx("div", { className: styles.descriptionPar, style: {
@@ -14,6 +14,36 @@
14
14
  overflow: hidden;
15
15
  text-overflow: ellipsis;
16
16
  }
17
+ .labelColumn {
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: flex-start;
21
+ flex: 1 1 auto;
22
+ min-width: 0;
23
+ margin-right: 10px;
24
+ margin-left: 10px;
25
+ }
26
+ .clampLbl,
27
+ .expandedLbl {
28
+ font-size: 18px;
29
+ }
30
+ .clampLbl {
31
+ display: -webkit-box;
32
+ -webkit-box-orient: vertical;
33
+ -webkit-line-clamp: 2;
34
+ line-clamp: 2;
35
+ overflow: hidden;
36
+ }
37
+ .toggleLbl {
38
+ background: none;
39
+ border: none;
40
+ padding: 0;
41
+ margin-top: 2px;
42
+ font-size: 13px;
43
+ font-weight: 700;
44
+ font-family: inherit;
45
+ cursor: pointer;
46
+ }
17
47
  .labelCheckBoxContainer {
18
48
  display: flex;
19
49
  align-items: center;
@@ -1,16 +1,17 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import styles from './MaterialPhoneInputStep.module.css';
3
- import { calcStepWidth } from '../../StepFunctions';
3
+ import { calcStepWidth, resolveStepSpan } from '../../StepFunctions';
4
4
  import { useContext } from 'react';
5
5
  import FormContext from '../../../Contexts/FormContext';
6
- import { selectBreakPoint, useAppSelector } from '../../../hooks';
6
+ import { selectWidthStats, useAppSelector } from '../../../hooks';
7
7
  import { useFormStep } from '../../StepHooks';
8
8
  import ErkPhoneInput from '../../../Shared/ErkPhoneInput/EkrPhoneInput';
9
9
  import { isEmptyPhoneNumber, isValidPhoneNumber } from '../../../Utils/PhoneFunctions';
10
10
  function PhoneInputStep({ step, editable, defaultValue = step.defaultValue }) {
11
- const currentBreakPoint = useAppSelector(selectBreakPoint);
11
+ const widthStats = useAppSelector(selectWidthStats);
12
12
  const postview = useAppSelector((state) => state.global.postview);
13
13
  const form = useContext(FormContext);
14
+ const span = resolveStepSpan(step, widthStats, form.size);
14
15
  const { ref, value, onChange, error, field } = useFormStep(step, {
15
16
  defaultValue: defaultValue ?? '',
16
17
  debounce: true,
@@ -26,7 +27,7 @@ function PhoneInputStep({ step, editable, defaultValue = step.defaultValue }) {
26
27
  },
27
28
  });
28
29
  return (_jsx("div", { className: styles.container, style: {
29
- width: currentBreakPoint <= step.size ? '100%' : calcStepWidth(step.size, form.size),
30
+ width: widthStats.currentBreakPoint <= span ? '100%' : calcStepWidth(span, form.size),
30
31
  minHeight: step.description || (!postview && editable) ? '55px' : '43px',
31
32
  }, children: _jsx(ErkPhoneInput, { ...field, value: value, inputRef: ref, error: !!error, labelMargin: 0, label: step.label, onChange: onChange, "data-testid": step.id, required: step.required, readOnly: !editable || postview, helperText: error?.message ?? step.description }) }));
32
33
  }
@@ -3,11 +3,25 @@ import { ApiSelector, CheckBox, ClassifierSelector, EntityValuePicker, FormSelec
3
3
  import { AYFFormStep } from '../@Types/AYFFormStep';
4
4
  import { CBRFormStep } from '../@Types/CBRFormStep';
5
5
  import { Condition } from '../@Types/Condition';
6
- import { DependencyStore } from '../Form/Form';
6
+ import { DependencyStore, WidthStats } from '../Form/Form';
7
+ import { GBaseStep } from '../@Types/GenericFormSteps';
7
8
  import { Entity } from '../@Types/Entity';
8
9
  export type FillerSteps = FormSelector | ClassifierSelector | EntityValuePicker | ApiSelector | CheckBox;
9
10
  export declare function calcFillerSize(step: FillerSteps, steps: Record<string, FormStep>, values: Record<string, unknown>, size: FormSize): number;
10
11
  export declare const calcStepWidth: (stepSize: 1 | 2 | 3 | 4, size: FormSize) => number;
12
+ /** Column span of a step inside the form grid (the grid itself has at most 4 columns). */
13
+ export type FormSpan = 1 | 2 | 3 | 4;
14
+ /** Steps that occupy form-grid columns; content-only steps (e.g. titles) may omit `size`. */
15
+ interface SpannedStep {
16
+ size?: FormSpan;
17
+ sizes?: GBaseStep['sizes'];
18
+ }
19
+ /**
20
+ * Effective column span of a step at the current width: its explicit `sizes` override for the
21
+ * screen when present (clamped to that screen's columns), the derived legacy default otherwise.
22
+ * Desktop never reads `sizes` — its source of truth stays the legacy `size` field.
23
+ */
24
+ export declare function resolveStepSpan(step: SpannedStep, widthStats: Pick<WidthStats, 'currentBreakPoint' | 'isMobile'>, formSize: FormSize): FormSpan;
11
25
  export declare const calcDefaultValue: (step: FormStep | CBRFormStep) => any;
12
26
  export declare const iterateNestedSteps: (idStep: string, steps: Record<string, FormStep>, iteration: (step: FormStep, path: string[]) => void, path?: string[]) => void;
13
27
  /**
@@ -25,3 +39,4 @@ export declare const evaluateCondition: (condition: Condition, dependencies: Dep
25
39
  } & Record<string, any>;
26
40
  }) => boolean;
27
41
  export declare function getRelativeDate(nDays?: number, nHours?: number, nMinutes?: number, subtract?: boolean, date?: Date): Date;
42
+ export {};
@@ -97,6 +97,64 @@ function recursivelyCheckOpenSize(idStep, steps, values) {
97
97
  export const calcStepWidth = (stepSize, size) => {
98
98
  return size.blockSize * stepSize + size.spacingSize * (stepSize - 1);
99
99
  };
100
+ const clampSpan = (span, columns) => Math.max(1, Math.min(span, columns));
101
+ /**
102
+ * Columns the form grid has on each screen — the editor's model, mirrored so both sides agree on
103
+ * what fits: desktop uses the form's configured `blockNum`, tablet caps it at 2, and mobile is the
104
+ * single column this renderer has always collapsed to.
105
+ */
106
+ function columnsForBreakpoint(formSize, breakpoint) {
107
+ switch (breakpoint) {
108
+ case 'desktop':
109
+ return formSize.blockNum;
110
+ case 'tablet':
111
+ return clampSpan(formSize.blockNum, 2);
112
+ case 'mobile':
113
+ return 1;
114
+ }
115
+ }
116
+ /**
117
+ * Span a step gets on a screen when it has no explicit override — the legacy model translated to
118
+ * the responsive one: desktop keeps the step's `size` (falling back to full width, as steps
119
+ * without one have always rendered), and the other screens shrink that span to fit their column
120
+ * count.
121
+ */
122
+ function deriveDefaultSpan(step, breakpoint, formSize) {
123
+ const desktopSpan = clampSpan(step.size ?? formSize.blockNum, formSize.blockNum);
124
+ if (breakpoint === 'desktop')
125
+ return desktopSpan;
126
+ return clampSpan(desktopSpan, columnsForBreakpoint(formSize, breakpoint));
127
+ }
128
+ /**
129
+ * Maps the renderer's width model onto the editor's. The editor lays out exactly three screens,
130
+ * while `currentBreakPoint` counts how many columns currently fit, so the two need a bridge:
131
+ * `isMobile` is the editor's mobile screen, and because the editor's tablet layout is a 2-column
132
+ * grid (see `columnsForBreakpoint`) its overrides apply only once at most 2 columns fit.
133
+ * Reduced-but-wider states (e.g. 3 columns of a 4-column form) keep the desktop spans — the
134
+ * editor has no screen for them, and this keeps forms without overrides rendering exactly as
135
+ * they always have.
136
+ */
137
+ function breakpointForWidthStats(widthStats, formSize) {
138
+ if (widthStats.isMobile)
139
+ return 'mobile';
140
+ if (widthStats.currentBreakPoint < formSize.blockNum && widthStats.currentBreakPoint <= 2)
141
+ return 'tablet';
142
+ return 'desktop';
143
+ }
144
+ /**
145
+ * Effective column span of a step at the current width: its explicit `sizes` override for the
146
+ * screen when present (clamped to that screen's columns), the derived legacy default otherwise.
147
+ * Desktop never reads `sizes` — its source of truth stays the legacy `size` field.
148
+ */
149
+ export function resolveStepSpan(step, widthStats, formSize) {
150
+ const breakpoint = breakpointForWidthStats(widthStats, formSize);
151
+ if (breakpoint !== 'desktop') {
152
+ const override = step.sizes?.[breakpoint];
153
+ if (override !== undefined)
154
+ return clampSpan(override, columnsForBreakpoint(formSize, breakpoint));
155
+ }
156
+ return deriveDefaultSpan(step, breakpoint, formSize);
157
+ }
100
158
  export const calcDefaultValue = (step) => {
101
159
  switch (step.type) {
102
160
  case FormStepTypes.CHECKBOX:
@@ -2,7 +2,7 @@ import AYFFormStepTypes from '../constants/AYFFormStepTypes';
2
2
  import CBRFormStepTypes from '../constants/CBRFormStepTypes';
3
3
  import FormStepTypes, { ApiSelectorOptionTypes, ClassifierOptionTypes, EntityValueOptionTypes, OptionTypes, } from '../constants/FormStepTypes';
4
4
  import { describe, expect, test, vi } from 'vitest';
5
- import { calcDefaultValue, calcFillerSize, calcStepWidth, calcSubSteps, iterateNestedSteps } from './StepFunctions';
5
+ import { calcDefaultValue, calcFillerSize, calcStepWidth, calcSubSteps, iterateNestedSteps, resolveStepSpan, } from './StepFunctions';
6
6
  import { stringToDraft } from '../Utils/DraftFunctions';
7
7
  describe('StepFunctions', function () {
8
8
  describe('calcFillerSize', function () {
@@ -411,6 +411,37 @@ describe('StepFunctions', function () {
411
411
  expect(size).toBe(440);
412
412
  });
413
413
  });
414
+ describe('resolveStepSpan', function () {
415
+ const fourColumns = { blockNum: 4, blockSize: 210, spacingSize: 20 };
416
+ const desktop = { currentBreakPoint: 4, isMobile: false };
417
+ const threeColumns = { currentBreakPoint: 3, isMobile: false };
418
+ const tablet = { currentBreakPoint: 2, isMobile: false };
419
+ const mobile = { currentBreakPoint: 1, isMobile: true };
420
+ test('Legacy step derives every screen from the scalar size', async () => {
421
+ const step = { size: 3 };
422
+ expect(resolveStepSpan(step, desktop, fourColumns)).toBe(3);
423
+ expect(resolveStepSpan(step, tablet, fourColumns)).toBe(2);
424
+ expect(resolveStepSpan(step, mobile, fourColumns)).toBe(1);
425
+ });
426
+ test('Step without size falls back to full width', async () => {
427
+ expect(resolveStepSpan({}, desktop, fourColumns)).toBe(4);
428
+ expect(resolveStepSpan({}, tablet, fourColumns)).toBe(2);
429
+ });
430
+ test('Tablet override applies once at most 2 columns fit', async () => {
431
+ const step = { size: 2, sizes: { tablet: 1 } };
432
+ expect(resolveStepSpan(step, tablet, fourColumns)).toBe(1);
433
+ expect(resolveStepSpan(step, desktop, fourColumns)).toBe(2);
434
+ });
435
+ test('Reduced-but-wider states keep the desktop span', async () => {
436
+ const step = { size: 3, sizes: { tablet: 1 } };
437
+ expect(resolveStepSpan(step, threeColumns, fourColumns)).toBe(3);
438
+ });
439
+ test('Overrides are clamped to the columns of their screen', async () => {
440
+ const step = { size: 1, sizes: { tablet: 4, mobile: 3 } };
441
+ expect(resolveStepSpan(step, tablet, fourColumns)).toBe(2);
442
+ expect(resolveStepSpan(step, mobile, fourColumns)).toBe(1);
443
+ });
444
+ });
414
445
  describe('calcDefaultValue', function () {
415
446
  test('CheckBox', async () => {
416
447
  const defaultValue = calcDefaultValue({
@@ -1,15 +1,16 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import ErkTextField from '../../../Shared/ErkTextField/ErkTextField';
3
3
  import styles from './MaterialTextInputStep.module.css';
4
- import { calcStepWidth } from '../../StepFunctions';
4
+ import { calcStepWidth, resolveStepSpan } from '../../StepFunctions';
5
5
  import { useContext } from 'react';
6
6
  import FormContext from '../../../Contexts/FormContext';
7
- import { selectBreakPoint, useAppSelector } from '../../../hooks';
7
+ import { selectWidthStats, useAppSelector } from '../../../hooks';
8
8
  import { useFormStep } from '../../StepHooks';
9
9
  function TextInputStep({ step, editable, maxLength, IconComponent, defaultValue = step.defaultValue, validation = step.validation, }) {
10
- const currentBreakPoint = useAppSelector(selectBreakPoint);
10
+ const widthStats = useAppSelector(selectWidthStats);
11
11
  const { postview } = useAppSelector((state) => state.global);
12
12
  const form = useContext(FormContext);
13
+ const span = resolveStepSpan(step, widthStats, form.size);
13
14
  const { ref, value, onChange, error, field } = useFormStep(step, {
14
15
  defaultValue: defaultValue ?? '',
15
16
  debounce: true,
@@ -27,7 +28,7 @@ function TextInputStep({ step, editable, maxLength, IconComponent, defaultValue
27
28
  },
28
29
  });
29
30
  return (_jsx("div", { className: styles.container, style: {
30
- width: currentBreakPoint <= step.size ? '100%' : calcStepWidth(step.size, form.size),
31
+ width: widthStats.currentBreakPoint <= span ? '100%' : calcStepWidth(span, form.size),
31
32
  minHeight: step.description || (!postview && editable && (step.required || validation)) ? '55px' : '43px',
32
33
  }, children: _jsx(ErkTextField, { ...field, value: value, inputRef: ref, labelMargin: 0, error: !!error, label: step.label, onChange: onChange, maxLength: maxLength, "data-testid": step.id, required: step.required, readOnly: !editable || postview, helperText: error?.message ?? step.description, IconComponent: step.showIcon ? IconComponent : undefined }) }));
33
34
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { calcStepWidth } from '../../StepFunctions';
2
+ import { calcStepWidth, resolveStepSpan } from '../../StepFunctions';
3
3
  import styles from './MaterialTitleStep.module.css';
4
4
  import { useAppSelector } from '../../../hooks';
5
5
  import FormContext from '../../../Contexts/FormContext';
@@ -16,7 +16,7 @@ function TitleStep({ step }) {
16
16
  const { formStyle } = useAppSelector((state) => state.global);
17
17
  const form = useContext(FormContext);
18
18
  const widthStats = useAppSelector((state) => state.widthStats);
19
- const size = step.size ?? form.size.blockNum;
19
+ const span = resolveStepSpan(step, widthStats, form.size);
20
20
  const title = useMemo(() => calcBaseDraft(value?.title ?? step.title), //Usa por defecto lo que venga en el value, pero no escucha cambios
21
21
  [step.title, value?.title]);
22
22
  const sourceTitle = useMemo(() => calcBaseDraft(step.title), [step.title]);
@@ -36,9 +36,9 @@ function TitleStep({ step }) {
36
36
  const path = useMemo(() => [...step.stepPath, step.id].join('.'), [step.stepPath, step.id]);
37
37
  return (_jsxs("div", { className: styles.container, style: {
38
38
  color: formStyle.textColor,
39
- width: widthStats.currentBreakPoint <= size ? '100%' : calcStepWidth(size, form.size),
39
+ width: widthStats.currentBreakPoint <= span ? '100%' : calcStepWidth(span, form.size),
40
40
  }, children: [_jsx("div", { className: styles.titleLbl, style: {
41
- textAlign: widthStats.isMobile && widthStats.currentBreakPoint <= size ? 'center' : 'start',
41
+ textAlign: widthStats.isMobile && widthStats.currentBreakPoint <= span ? 'center' : 'start',
42
42
  }, children: _jsx(SmartDraftRenderer, { draft: title, sourceDraft: sourceTitle, dependencies: values, property: `${path}.title`, onChange: (title) => onChange({ ...value, title }) }) }), _jsx("div", { className: styles.descriptionPar, children: _jsx(SmartDraftRenderer, { draft: description, sourceDraft: sourceDescription, dependencies: values, margin: title ? '10px 0px' : '0px 0px 5px 0px', property: `${path}.description`, onChange: (description) => onChange({ ...value, description }) }) })] }));
43
43
  }
44
44
  export default TitleStep;
@@ -1,23 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useContext, useState } from 'react';
3
3
  import { useFormContext } from 'react-hook-form';
4
- import { calcFillerSize } from '../../StepFunctions';
4
+ import { calcFillerSize, resolveStepSpan } from '../../StepFunctions';
5
5
  import FormContext from '../../../Contexts/FormContext';
6
6
  import styles from './StepFiller.module.css';
7
- import { selectBreakPoint, useAppSelector } from '../../../hooks';
7
+ import { selectWidthStats, useAppSelector } from '../../../hooks';
8
8
  import { SizeChangeContext } from './StepFillerContext';
9
9
  function StepFiller({ step, children }) {
10
10
  const { getValues } = useFormContext();
11
11
  const form = useContext(FormContext);
12
12
  const [fillerSize, setFillerSize] = useState(calcFillerSize(step, form.steps, getValues(), form.size));
13
- const currentBreakPoint = useAppSelector(selectBreakPoint);
13
+ const widthStats = useAppSelector(selectWidthStats);
14
+ const span = resolveStepSpan(step, widthStats, form.size);
14
15
  const handleSizeChange = useCallback(() => {
15
16
  setFillerSize(calcFillerSize(step, form.steps, getValues(), form.size));
16
17
  // eslint-disable-next-line react-hooks/exhaustive-deps
17
18
  }, []);
18
19
  if (step.maxSize && step.maxSize < form.size.blockNum) {
19
20
  return (_jsxs("div", { className: styles.firstLvlContainer, style: {
20
- width: currentBreakPoint <= step.size ? '100%' : 'fit-content',
21
+ width: widthStats.currentBreakPoint <= span ? '100%' : 'fit-content',
21
22
  }, children: [_jsx(SizeChangeContext.Provider, { value: handleSizeChange, children: children }), fillerSize > 0 && (_jsx("div", { className: styles.smallSeparator, style: {
22
23
  width: fillerSize,
23
24
  } }))] }));
@@ -1,15 +1,16 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useContext } from 'react';
3
3
  import FormContext from '../../../Contexts/FormContext';
4
- import { useAppSelector, selectBreakPoint } from '../../../hooks';
5
- import { calcStepWidth } from '../../StepFunctions';
4
+ import { useAppSelector, selectWidthStats } from '../../../hooks';
5
+ import { calcStepWidth, resolveStepSpan } from '../../StepFunctions';
6
6
  import styles from './MaterialInputContainer.module.css';
7
7
  function MaterialInputContainer({ step, editable, children, onClick, maxHeight = true, }) {
8
8
  const form = useContext(FormContext);
9
- const currentBreakPoint = useAppSelector(selectBreakPoint);
9
+ const widthStats = useAppSelector(selectWidthStats);
10
10
  const postview = useAppSelector((state) => state.global.postview);
11
+ const span = resolveStepSpan(step, widthStats, form.size);
11
12
  return (_jsx("div", { className: styles.container, style: {
12
- width: currentBreakPoint <= step.size ? '100%' : calcStepWidth(step.size, form.size),
13
+ width: widthStats.currentBreakPoint <= span ? '100%' : calcStepWidth(span, form.size),
13
14
  minHeight: maxHeight
14
15
  ? step.description || (!postview && editable && step.required)
15
16
  ? '55px'
@@ -1,19 +1,17 @@
1
1
  import CBRFormStepTypes from '../constants/CBRFormStepTypes';
2
2
  export const calcCbrForm = (form, formValues) => {
3
- /** Callers pass the form straight out of `createNextState`, whose result Immer deep-freezes. */
4
- const steps = { ...form.steps };
5
3
  for (const step of Object.values(form.steps)) {
6
4
  if (step.type === CBRFormStepTypes.CBR_INCIDENCIAS) {
7
5
  const incidentStep = step;
8
6
  const incValue = formValues[incidentStep.id ?? ''];
9
7
  if (incValue) {
10
8
  for (const incident of incValue) {
11
- handleAddIncident(incident, incidentStep, steps, formValues);
9
+ handleAddIncident(incident, incidentStep, form.steps, formValues);
12
10
  }
13
11
  }
14
12
  }
15
13
  }
16
- return { ...form, steps };
14
+ return form;
17
15
  };
18
16
  const handleAddIncident = (incident, step, steps, formValues) => {
19
17
  const spaceStep = {
package/dist/hooks.d.ts CHANGED
@@ -5,4 +5,5 @@ import { QueryActionCreatorResult, QueryDefinition, ResultTypeFrom } from '@redu
5
5
  export declare const useAppDispatch: () => AppDispatch;
6
6
  export declare const useAppSelector: TypedUseSelectorHook<RootState>;
7
7
  export declare const selectBreakPoint: (state: RootState) => number;
8
+ export declare const selectWidthStats: (state: RootState) => RootState["widthStats"];
8
9
  export declare function useApiSubscribe(): <D extends QueryDefinition<any, any, any, any>>(subscription: ThunkAction<QueryActionCreatorResult<D>, any, any, any>) => Promise<ResultTypeFrom<D> | undefined>;
package/dist/hooks.js CHANGED
@@ -20,6 +20,7 @@ export const useAppSelector = (selector, options) => {
20
20
  return useSelector((state) => selector(state.forms[idForm] ?? defaultRootState), options);
21
21
  };
22
22
  export const selectBreakPoint = (state) => state.widthStats.currentBreakPoint;
23
+ export const selectWidthStats = (state) => state.widthStats;
23
24
  export function useApiSubscribe() {
24
25
  const dispatch = useAppDispatch();
25
26
  const subscriptions = useRef([]);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arquimedes.co/eureka-forms",
3
3
  "repository": "git://github.com/Arquimede5/Eureka-Forms.git",
4
- "version": "3.0.55",
4
+ "version":"3.0.56-test",
5
5
  "scripts": {
6
6
  "watch": "tsgo --noEmit --watch --project tsconfig.app.json",
7
7
  "start": "vite",
@@ -1,78 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { render, screen, waitFor } from '@testing-library/react';
3
- import { describe, expect, it } from 'vitest';
4
- import '@testing-library/jest-dom';
5
- import App from './App';
6
- import CBRFormStepTypes from '../constants/CBRFormStepTypes';
7
- const INCIDENT = {
8
- idSpaceStep: 'CBR_TIPO_ESPACIO-PYDqn',
9
- idElementStep: 'CBR_LOCATIVAS-fp6xL',
10
- idCommentStep: 'CBR_COMENTARIO-sQjpP',
11
- num: 1,
12
- };
13
- const ID_SUB_ELEMENT_STEP = 'CBR_LOCATIVAS-G4S0r';
14
- /** Shaped after a real posventa ticket: incidents are stored flat, keyed by generated step ids. */
15
- const POSVENTA_VALUES = {
16
- CBR_INCIDENCIAS: [INCIDENT],
17
- [INCIDENT.idSpaceStep]: { id: '35365', label: 'Zona de Oficios' },
18
- [INCIDENT.idCommentStep]: 'GRAN HUMEDAD!!!',
19
- [INCIDENT.idElementStep]: { id: '06', label: 'CIELOS', subStep: ID_SUB_ELEMENT_STEP },
20
- [ID_SUB_ELEMENT_STEP]: { id: '0601', label: 'Cielo Falso' },
21
- };
22
- const POSVENTA_FORM = {
23
- name: 'Formulario PosVenta',
24
- firstSection: 'FIRST',
25
- sections: {
26
- FIRST: { id: 'FIRST', name: '', nextSection: null, steps: ['CBR_INCIDENCIAS'] },
27
- },
28
- steps: {
29
- CBR_INCIDENCIAS: {
30
- id: 'CBR_INCIDENCIAS',
31
- type: CBRFormStepTypes.CBR_INCIDENCIAS,
32
- label: 'Incidencias',
33
- description: null,
34
- addBtnLabel: 'Agregar Incidencia',
35
- required: true,
36
- unitLabel: 'Incidencia',
37
- dependencies: ['CBR_PROYECTO'],
38
- spaceStep: {
39
- label: 'Espacio',
40
- description: null,
41
- size: 1,
42
- required: true,
43
- showIcon: true,
44
- searchable: true,
45
- },
46
- commentStep: { label: 'Comentario', description: null, size: 1, required: true },
47
- elementStep: {
48
- label: 'Elemento',
49
- description: null,
50
- size: 1,
51
- required: true,
52
- showIcon: true,
53
- searchable: true,
54
- subStep: {
55
- label: 'Elemento Hijo',
56
- description: null,
57
- size: 1,
58
- required: true,
59
- showIcon: true,
60
- searchable: true,
61
- },
62
- },
63
- },
64
- },
65
- size: { blockNum: 4, blockSize: 210, spacingSize: 20 },
66
- };
67
- describe('CBR posventa postview', () => {
68
- it('renders the submitted incidents read-only', async () => {
69
- const { container } = render(_jsx(App, { formData: POSVENTA_FORM, valuesData: POSVENTA_VALUES, idOrganization: 'melendez', postview: true, internal: true, isWidget: true, editable: false }));
70
- await waitFor(() => {
71
- expect(container.textContent).toContain('Incidencia 1');
72
- });
73
- expect(container.textContent).toContain('GRAN HUMEDAD!!!');
74
- expect(screen.getByDisplayValue('Zona de Oficios')).toBeInTheDocument();
75
- expect(screen.getByDisplayValue('CIELOS')).toBeInTheDocument();
76
- expect(screen.getByDisplayValue('Cielo Falso')).toBeInTheDocument();
77
- });
78
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,123 +0,0 @@
1
- import { createNextState } from '@reduxjs/toolkit';
2
- import { describe, expect, test } from 'vitest';
3
- import CBRFormStepTypes from '../constants/CBRFormStepTypes';
4
- import { calcCbrForm } from './CBRFunctions';
5
- const INCIDENT = {
6
- idSpaceStep: 'CBR_TIPO_ESPACIO-space1',
7
- idElementStep: 'CBR_LOCATIVAS-element1',
8
- idCommentStep: 'CBR_COMENTARIO-comment1',
9
- num: 1,
10
- };
11
- /** Second element level, reached through the element value's `subStep` pointer. */
12
- const ID_SUB_ELEMENT_STEP = 'CBR_LOCATIVAS-subElement1';
13
- const FORM_VALUES = {
14
- CBR_INCIDENCIAS: [INCIDENT],
15
- [INCIDENT.idSpaceStep]: { id: '35362', label: 'Sala' },
16
- [INCIDENT.idCommentStep]: 'GRAN HUMEDAD',
17
- [INCIDENT.idElementStep]: {
18
- id: '06',
19
- label: 'CIELOS',
20
- subStep: ID_SUB_ELEMENT_STEP,
21
- },
22
- [ID_SUB_ELEMENT_STEP]: { id: '0601', label: 'Cielo Falso' },
23
- };
24
- function buildForm() {
25
- return {
26
- name: 'Formulario PosVenta',
27
- firstSection: 'FIRST',
28
- sections: {
29
- FIRST: {
30
- id: 'FIRST',
31
- name: '',
32
- nextSection: null,
33
- steps: ['CBR_INCIDENCIAS'],
34
- },
35
- },
36
- steps: {
37
- CBR_INCIDENCIAS: {
38
- id: 'CBR_INCIDENCIAS',
39
- type: CBRFormStepTypes.CBR_INCIDENCIAS,
40
- label: 'Incidencias',
41
- description: null,
42
- addBtnLabel: 'Agregar',
43
- required: true,
44
- unitLabel: 'Incidencia',
45
- dependencies: ['CBR_PROYECTO'],
46
- spaceStep: {
47
- label: 'Espacio',
48
- description: null,
49
- size: 1,
50
- required: true,
51
- showIcon: true,
52
- searchable: true,
53
- },
54
- commentStep: {
55
- label: 'Comentario',
56
- description: null,
57
- size: 1,
58
- required: true,
59
- },
60
- elementStep: {
61
- label: 'Elemento',
62
- description: null,
63
- size: 1,
64
- required: true,
65
- showIcon: true,
66
- searchable: true,
67
- subStep: {
68
- label: 'Elemento Hijo',
69
- description: null,
70
- size: 1,
71
- required: true,
72
- showIcon: true,
73
- searchable: true,
74
- },
75
- },
76
- },
77
- },
78
- size: { blockNum: 4, blockSize: 210, spacingSize: 20 },
79
- };
80
- }
81
- describe('calcCbrForm', function () {
82
- test('expands every incident into its own space, comment and element steps', () => {
83
- const result = calcCbrForm(buildForm(), FORM_VALUES);
84
- expect(result.steps[INCIDENT.idSpaceStep]).toMatchObject({
85
- id: INCIDENT.idSpaceStep,
86
- type: CBRFormStepTypes.CBR_TIPO_ESPACIO,
87
- });
88
- expect(result.steps[INCIDENT.idCommentStep]).toMatchObject({
89
- id: INCIDENT.idCommentStep,
90
- type: CBRFormStepTypes.CBR_COMENTARIO,
91
- });
92
- expect(result.steps[INCIDENT.idElementStep]).toMatchObject({
93
- id: INCIDENT.idElementStep,
94
- type: CBRFormStepTypes.CBR_LOCATIVAS,
95
- subStep: ID_SUB_ELEMENT_STEP,
96
- });
97
- expect(result.steps[ID_SUB_ELEMENT_STEP]).toMatchObject({
98
- id: ID_SUB_ELEMENT_STEP,
99
- type: CBRFormStepTypes.CBR_LOCATIVAS,
100
- });
101
- });
102
- test('leaves the form it was given untouched', () => {
103
- const form = buildForm();
104
- calcCbrForm(form, FORM_VALUES);
105
- expect(Object.keys(form.steps)).toEqual(['CBR_INCIDENCIAS']);
106
- });
107
- /**
108
- * The caller hands the form straight out of `createNextState`, whose result Immer deep-freezes, so
109
- * expanding the incidents by writing into that same `steps` object throws and leaves the form stuck
110
- * loading forever.
111
- */
112
- test('expands a form frozen by createNextState', () => {
113
- const frozenForm = createNextState(buildForm(), (draft) => {
114
- draft.steps.CBR_INCIDENCIAS.idSection = 'FIRST';
115
- });
116
- expect(Object.isFrozen(frozenForm.steps)).toBe(true);
117
- const result = calcCbrForm(frozenForm, FORM_VALUES);
118
- expect(result.steps[INCIDENT.idSpaceStep]).toBeDefined();
119
- expect(result.steps[INCIDENT.idCommentStep]).toBeDefined();
120
- expect(result.steps[INCIDENT.idElementStep]).toBeDefined();
121
- expect(result.steps[ID_SUB_ELEMENT_STEP]).toBeDefined();
122
- });
123
- });