@escapenavigator/hooks 2.2.9 → 2.2.10
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/dist/lead-form/lead-field.js +10 -5
- package/dist/lead-form/lead-form.d.ts +3 -2
- package/dist/lead-form/lead-form.js +61 -18
- package/dist/lead-form/lib.d.ts +1 -1
- package/dist/lead-form/lib.js +6 -20
- package/dist/lead-form/lib.spec.js +18 -0
- package/dist/lead-form/use-lead-t.js +5 -1
- package/dist/use-localstore-state/index.d.ts +2 -0
- package/dist/use-localstore-state/index.js +27 -8
- package/dist/use-localstore-state/index.spec.d.ts +1 -0
- package/dist/use-localstore-state/index.spec.js +30 -0
- package/package.json +5 -5
|
@@ -81,11 +81,16 @@ const ChoiceIcon = ({ id }) => {
|
|
|
81
81
|
* Радио и чекбоксы площадки — кнопками-вариантами: один тап вместо попадания
|
|
82
82
|
* в кружок, и форма читается как выбор, а не как анкета.
|
|
83
83
|
*/
|
|
84
|
-
const ChoiceGroup = ({ label, options, isSelected, onToggle, hint, error, }) => {
|
|
85
|
-
const tiled =
|
|
84
|
+
const ChoiceGroup = ({ presentation, label, options, isSelected, onToggle, hint, error, }) => {
|
|
85
|
+
const tiled = presentation === 'cards' ||
|
|
86
|
+
((!presentation || presentation === 'auto') && options.length >= TILES_FROM);
|
|
86
87
|
return (react_1.default.createElement(ui_1.FlexColumns, { columns: 1, gr: 8 },
|
|
87
88
|
react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', weight: 'medium', color: 'primary' }, label),
|
|
88
|
-
react_1.default.createElement("div", { role: 'group', "aria-label": label, className:
|
|
89
|
+
react_1.default.createElement("div", { role: 'group', "aria-label": label, className: presentation === 'list'
|
|
90
|
+
? index_module_css_1.default.choiceList
|
|
91
|
+
: tiled
|
|
92
|
+
? index_module_css_1.default.choiceTiles
|
|
93
|
+
: index_module_css_1.default.choicePills }, options.map((option) => {
|
|
89
94
|
const selected = isSelected(option.id);
|
|
90
95
|
return (react_1.default.createElement("button", { key: option.id, type: 'button', "aria-pressed": selected, className: index_module_css_1.default.choice, "data-selected": selected, "data-invalid": !!error, onClick: () => onToggle(option.id) },
|
|
91
96
|
tiled && react_1.default.createElement(ChoiceIcon, { id: option.id }),
|
|
@@ -260,10 +265,10 @@ const LeadField = ({ field, value, error, language, profileCountry, questrooms,
|
|
|
260
265
|
content: (0, lib_1.optionLabel)(option, language, option.id),
|
|
261
266
|
})), selected: String(value || ''), error: error, onChange: ({ selected }) => onChange(selected === null || selected === void 0 ? void 0 : selected.key) }));
|
|
262
267
|
case lead_field_type_enum_1.LeadFieldTypeEnum.RADIO:
|
|
263
|
-
return (react_1.default.createElement(ChoiceGroup, { label: label, options: choiceOptions, isSelected: (id) => value === id, onToggle: (id) => onChange(id), hint: hint, error: error }));
|
|
268
|
+
return (react_1.default.createElement(ChoiceGroup, { presentation: field.presentation, label: label, options: choiceOptions, isSelected: (id) => value === id, onToggle: (id) => onChange(id), hint: hint, error: error }));
|
|
264
269
|
case lead_field_type_enum_1.LeadFieldTypeEnum.CHECKBOX: {
|
|
265
270
|
const selected = Array.isArray(value) ? value : [];
|
|
266
|
-
return (react_1.default.createElement(ChoiceGroup, { label: label, options: choiceOptions, isSelected: (id) => selected.includes(id), onToggle: (id) => onChange(selected.includes(id)
|
|
271
|
+
return (react_1.default.createElement(ChoiceGroup, { presentation: field.presentation, label: label, options: choiceOptions, isSelected: (id) => selected.includes(id), onToggle: (id) => onChange(selected.includes(id)
|
|
267
272
|
? selected.filter((item) => item !== id)
|
|
268
273
|
: [...selected, id]), hint: hint, error: error }));
|
|
269
274
|
}
|
|
@@ -47,8 +47,9 @@ type Props = {
|
|
|
47
47
|
/** Документ согласия на обработку ПДн (галочка есть только в РФ). */
|
|
48
48
|
pdConsentUrl?: string;
|
|
49
49
|
/** Модалка управляет шагом, чтобы использовать свою кнопку «Назад». */
|
|
50
|
-
currentStep?:
|
|
51
|
-
|
|
50
|
+
currentStep?: number;
|
|
51
|
+
preview?: boolean;
|
|
52
|
+
onStepChange?: (step: number) => void;
|
|
52
53
|
showBackButton?: boolean;
|
|
53
54
|
};
|
|
54
55
|
export declare const LeadForm: React.FC<Props>;
|
|
@@ -49,6 +49,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
49
49
|
exports.LeadForm = void 0;
|
|
50
50
|
const ui_1 = require("@alphakits/ui");
|
|
51
51
|
const lead_field_type_enum_1 = require("@escapenavigator/types/dist/lead/enums/lead-field-type.enum");
|
|
52
|
+
const lead_form_layout_1 = require("@escapenavigator/types/dist/lead/lead-form-layout");
|
|
52
53
|
const react_1 = __importStar(require("react"));
|
|
53
54
|
const personal_data_consent_1 = require("../personal-data-consent");
|
|
54
55
|
const index_module_css_1 = __importDefault(require("./index.module.css"));
|
|
@@ -77,17 +78,18 @@ const CONTACT_FIELD_ORDER = {
|
|
|
77
78
|
[lead_field_type_enum_1.LeadFieldTypeEnum.EMAIL]: 6,
|
|
78
79
|
[lead_field_type_enum_1.LeadFieldTypeEnum.CONSENT]: 7,
|
|
79
80
|
};
|
|
80
|
-
const LeadForm = ({ form, source, language, profileCountry, submit, prefill, contextNote, withHeader = true, onDone, doneLabel, onSuccess, policyUrl, pdConsentUrl, currentStep, onStepChange, showBackButton = true, }) => {
|
|
81
|
+
const LeadForm = ({ form, source, language, profileCountry, submit, prefill, contextNote, withHeader = true, onDone, doneLabel, onSuccess, policyUrl, pdConsentUrl, currentStep, onStepChange, showBackButton = true, preview = false, }) => {
|
|
82
|
+
var _a, _b;
|
|
81
83
|
const t = (0, use_lead_t_1.useLeadT)();
|
|
82
84
|
const consentRequired = (0, personal_data_consent_1.requiresPersonalDataConsent)(profileCountry);
|
|
83
85
|
const [consent, setConsent] = (0, react_1.useState)(false);
|
|
84
86
|
const [showConsentError, setShowConsentError] = (0, react_1.useState)(false);
|
|
85
87
|
const [internalStep, setInternalStep] = (0, react_1.useState)(1);
|
|
86
|
-
const
|
|
87
|
-
const setStep = (nextStep) => {
|
|
88
|
+
const requestedStep = currentStep !== null && currentStep !== void 0 ? currentStep : internalStep;
|
|
89
|
+
const setStep = (0, react_1.useCallback)((nextStep) => {
|
|
88
90
|
setInternalStep(nextStep);
|
|
89
91
|
onStepChange === null || onStepChange === void 0 ? void 0 : onStepChange(nextStep);
|
|
90
|
-
};
|
|
92
|
+
}, [onStepChange]);
|
|
91
93
|
const [values, setValues] = (0, react_1.useState)(() => (0, lib_1.buildInitialValues)(form, prefill));
|
|
92
94
|
const [errors, setErrors] = (0, react_1.useState)({});
|
|
93
95
|
const [formError, setFormError] = (0, react_1.useState)();
|
|
@@ -101,7 +103,7 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
101
103
|
setErrors((prev) => (prev[fieldId] ? Object.assign(Object.assign({}, prev), { [fieldId]: '' }) : prev));
|
|
102
104
|
setFormError(undefined);
|
|
103
105
|
};
|
|
104
|
-
const visibleFields = form.fields.filter((field) => (0, lib_1.isVisible)(field, values));
|
|
106
|
+
const visibleFields = form.fields.filter((field) => (0, lib_1.isVisible)(field, values, form.fields));
|
|
105
107
|
const planningFields = visibleFields.filter((field) => PLANNING_FIELD_IDS.has(field.id) || PLANNING_FIELD_TYPES.has(field.type));
|
|
106
108
|
const contactFields = visibleFields
|
|
107
109
|
.filter((field) => !PLANNING_FIELD_IDS.has(field.id) && !PLANNING_FIELD_TYPES.has(field.type))
|
|
@@ -110,10 +112,36 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
110
112
|
return ((_a = CONTACT_FIELD_ORDER[left.type]) !== null && _a !== void 0 ? _a : 10) - ((_b = CONTACT_FIELD_ORDER[right.type]) !== null && _b !== void 0 ? _b : 10) ||
|
|
111
113
|
left.order - right.order;
|
|
112
114
|
});
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
+
const layout = (0, lead_form_layout_1.normalizeLeadLayout)(form.layout, form.fields);
|
|
116
|
+
const configuredSteps = (layout === null || layout === void 0 ? void 0 : layout.mode) === 'steps'
|
|
117
|
+
? layout.steps
|
|
118
|
+
.map((item) => (Object.assign(Object.assign({}, item), { fields: item.fieldIds
|
|
119
|
+
.map((id) => visibleFields.find((field) => field.id === id))
|
|
120
|
+
.filter(Boolean) })))
|
|
121
|
+
.filter((item) => item.fields.length)
|
|
122
|
+
: [];
|
|
123
|
+
const legacyTwoSteps = !layout && planningFields.length > 0 && contactFields.length > 0;
|
|
124
|
+
const stepCount = layout ? Math.max(1, configuredSteps.length) : legacyTwoSteps ? 2 : 1;
|
|
125
|
+
const step = Math.min(Math.max(1, requestedStep), stepCount);
|
|
126
|
+
(0, react_1.useEffect)(() => {
|
|
127
|
+
if (requestedStep !== step)
|
|
128
|
+
setStep(step);
|
|
129
|
+
}, [requestedStep, step, setStep]);
|
|
130
|
+
const hasTwoSteps = stepCount > 1;
|
|
131
|
+
const activeFields = layout
|
|
132
|
+
? layout.mode === 'single'
|
|
133
|
+
? visibleFields
|
|
134
|
+
: ((_a = configuredSteps[step - 1]) === null || _a === void 0 ? void 0 : _a.fields) || []
|
|
135
|
+
: !hasTwoSteps
|
|
136
|
+
? visibleFields
|
|
137
|
+
: step === 1
|
|
138
|
+
? planningFields
|
|
139
|
+
: contactFields;
|
|
115
140
|
const planningFieldIds = new Set(planningFields.map((field) => field.id));
|
|
116
|
-
const
|
|
141
|
+
const summaryFields = layout
|
|
142
|
+
? configuredSteps.slice(0, -1).flatMap((item) => item.fields)
|
|
143
|
+
: planningFields;
|
|
144
|
+
const planningSummary = summaryFields
|
|
117
145
|
.map((field) => {
|
|
118
146
|
var _a, _b, _c;
|
|
119
147
|
const value = values[field.id];
|
|
@@ -151,7 +179,7 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
151
179
|
values,
|
|
152
180
|
phoneValid,
|
|
153
181
|
t,
|
|
154
|
-
fieldIds:
|
|
182
|
+
fieldIds: new Set(activeFields.map((field) => field.id)),
|
|
155
183
|
requireContact: false,
|
|
156
184
|
});
|
|
157
185
|
if (Object.keys(result.errors).length) {
|
|
@@ -160,7 +188,7 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
160
188
|
}
|
|
161
189
|
setErrors({});
|
|
162
190
|
setFormError(undefined);
|
|
163
|
-
setStep(
|
|
191
|
+
setStep(step + 1);
|
|
164
192
|
};
|
|
165
193
|
const handleSubmit = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
166
194
|
var _a;
|
|
@@ -172,11 +200,23 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
172
200
|
if (Object.keys(result.errors).length || result.formError || consentMissing) {
|
|
173
201
|
setErrors(result.errors);
|
|
174
202
|
setFormError(result.formError);
|
|
175
|
-
|
|
203
|
+
const invalidStep = configuredSteps.findIndex((item) => item.fields.some((field) => result.errors[field.id]));
|
|
204
|
+
if (layout && invalidStep >= 0)
|
|
205
|
+
setStep(invalidStep + 1);
|
|
206
|
+
if (!layout &&
|
|
207
|
+
Object.keys(result.errors).some((fieldId) => planningFieldIds.has(fieldId))) {
|
|
176
208
|
setStep(1);
|
|
177
209
|
}
|
|
178
210
|
return;
|
|
179
211
|
}
|
|
212
|
+
if (preview) {
|
|
213
|
+
setSuccess({
|
|
214
|
+
success: true,
|
|
215
|
+
successTitle: form.successTitle,
|
|
216
|
+
successDescription: form.successDescription,
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
180
220
|
setSending(true);
|
|
181
221
|
setFormError(undefined);
|
|
182
222
|
const response = yield submit({
|
|
@@ -207,30 +247,33 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
207
247
|
!!onDone && (react_1.default.createElement("div", { className: index_module_css_1.default.successActions },
|
|
208
248
|
react_1.default.createElement(ui_1.Button, { view: 'primary', size: 's', block: true, onClick: onDone }, doneLabel || t('lead.close'))))));
|
|
209
249
|
}
|
|
210
|
-
const title = (0, lib_1.localized)(
|
|
250
|
+
const title = (0, lib_1.localized)((_b = configuredSteps[step - 1]) === null || _b === void 0 ? void 0 : _b.title, language) ||
|
|
251
|
+
(0, lib_1.localized)(form.clientTitle, language) ||
|
|
252
|
+
t('lead.title');
|
|
211
253
|
const description = (0, lib_1.localized)(form.description, language);
|
|
212
254
|
const stepLabel = step === 1 ? t('lead.stepPlanning') : t('lead.stepContacts');
|
|
213
|
-
const showHeaderCopy = !hasTwoSteps || step === 1;
|
|
255
|
+
const showHeaderCopy = !!layout || !hasTwoSteps || step === 1;
|
|
214
256
|
const dateField = activeFields.find((field) => field.type === lead_field_type_enum_1.LeadFieldTypeEnum.DATE);
|
|
215
257
|
const timeField = activeFields.find((field) => field.type === lead_field_type_enum_1.LeadFieldTypeEnum.TIME);
|
|
216
258
|
return (react_1.default.createElement("div", { className: index_module_css_1.default.form },
|
|
217
259
|
withHeader && (react_1.default.createElement("div", { className: index_module_css_1.default.header },
|
|
218
260
|
hasTwoSteps ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
219
261
|
react_1.default.createElement("div", { className: index_module_css_1.default.stepMeta },
|
|
220
|
-
step
|
|
262
|
+
step > 1 && showBackButton ? (react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.backButton, onClick: () => setStep(step - 1) },
|
|
221
263
|
react_1.default.createElement(ArrowLeft, null),
|
|
222
264
|
t('lead.back'))) : (react_1.default.createElement("span", null)),
|
|
223
|
-
react_1.default.createElement("span", null, t('lead.stepCounter', {
|
|
265
|
+
react_1.default.createElement("span", null, t(layout ? 'lead.customStepCounter' : 'lead.stepCounter', {
|
|
266
|
+
total: stepCount,
|
|
224
267
|
current: step,
|
|
225
268
|
label: stepLabel,
|
|
226
269
|
}))),
|
|
227
270
|
react_1.default.createElement("div", { className: index_module_css_1.default.progress, "aria-hidden": 'true' },
|
|
228
|
-
react_1.default.createElement("span", { "data-step": step })))) : (react_1.default.createElement("span", { className: index_module_css_1.default.headerAccent, "aria-hidden": 'true' })),
|
|
271
|
+
react_1.default.createElement("span", { "data-step": step, style: { width: `${(step / stepCount) * 100}%` } })))) : (react_1.default.createElement("span", { className: index_module_css_1.default.headerAccent, "aria-hidden": 'true' })),
|
|
229
272
|
showHeaderCopy && (react_1.default.createElement("div", { className: index_module_css_1.default.headerCopy },
|
|
230
273
|
react_1.default.createElement(ui_1.Typography.Title, { tag: 'h1', view: 'small', weight: 'bold' }, title),
|
|
231
274
|
!!description && (react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', color: 'secondary' }, description)))))),
|
|
232
275
|
!!formError && react_1.default.createElement(ui_1.ToastPlate, { view: 'negative' }, formError),
|
|
233
|
-
hasTwoSteps && step ===
|
|
276
|
+
hasTwoSteps && step === stepCount && !!planningSummary.length && (react_1.default.createElement("div", { className: index_module_css_1.default.selectionSummary },
|
|
234
277
|
react_1.default.createElement("div", null,
|
|
235
278
|
react_1.default.createElement("span", { className: index_module_css_1.default.summaryIcon, "aria-hidden": 'true' },
|
|
236
279
|
react_1.default.createElement("svg", { viewBox: '0 0 24 24', fill: 'none', "aria-hidden": 'true' },
|
|
@@ -248,7 +291,7 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
248
291
|
: undefined, onChange: (value) => setValue(field.id, value), onPhoneValidChange: setPhoneValid })));
|
|
249
292
|
})),
|
|
250
293
|
react_1.default.createElement("input", { className: index_module_css_1.default.trap, tabIndex: -1, autoComplete: 'off', "aria-hidden": 'true', value: trap, onChange: (event) => setTrap(event.target.value) }),
|
|
251
|
-
react_1.default.createElement("div", { className: index_module_css_1.default.footer, "data-step": hasTwoSteps ? step : 1 },
|
|
294
|
+
react_1.default.createElement("div", { className: index_module_css_1.default.footer, "data-step": hasTwoSteps ? step : 1 }, step < stepCount ? (react_1.default.createElement(ui_1.Button, { view: 'primary', size: 's', block: true, rightAddons: react_1.default.createElement(ArrowRight, null), onClick: handleNext }, t(layout ? 'lead.continue' : 'lead.next'))) : (react_1.default.createElement(react_1.default.Fragment, null,
|
|
252
295
|
(consentRequired || !!policyUrl) && (react_1.default.createElement(personal_data_consent_1.PersonalDataConsent, { required: consentRequired, consentUrl: pdConsentUrl || '', policyUrl: policyUrl || '', checked: consent, showError: showConsentError, onChange: (checked) => {
|
|
253
296
|
setConsent(checked);
|
|
254
297
|
if (checked)
|
package/dist/lead-form/lib.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare const isMultiple: (type: LeadFieldTypeEnum) => boolean;
|
|
|
23
23
|
* поле не валидируется и не попадает в ответы — иначе обязательная
|
|
24
24
|
* «уточните какой квест» блокировала бы отправку у всех остальных.
|
|
25
25
|
*/
|
|
26
|
-
export declare const isVisible: (field: LeadFormField, values: LeadFormValues) => boolean;
|
|
26
|
+
export declare const isVisible: (field: LeadFormField, values: LeadFormValues, fields?: LeadFormField[]) => boolean;
|
|
27
27
|
/**
|
|
28
28
|
* Предзаполнение по смысловой роли поля, а не по его id: форма настраивается
|
|
29
29
|
* площадкой, и id полей у каждой свои. Роль (`mapsTo`) стабильна — по ней
|
package/dist/lead-form/lib.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.buildAnswers = exports.validate = exports.buildInitialValues = exports.isVisible = exports.isMultiple = exports.optionLabel = exports.fieldText = exports.fieldTitle = exports.localized = void 0;
|
|
4
4
|
const lead_field_mapping_enum_1 = require("@escapenavigator/types/dist/lead/enums/lead-field-mapping.enum");
|
|
5
5
|
const lead_field_type_enum_1 = require("@escapenavigator/types/dist/lead/enums/lead-field-type.enum");
|
|
6
|
+
const lead_form_layout_1 = require("@escapenavigator/types/dist/lead/lead-form-layout");
|
|
6
7
|
const languages_enum_1 = require("@escapenavigator/types/dist/shared/enum/languages.enum");
|
|
7
8
|
/**
|
|
8
9
|
* Текст на языке виджета. Площадка могла не перевести форму на все языки —
|
|
@@ -45,24 +46,9 @@ exports.isMultiple = isMultiple;
|
|
|
45
46
|
* поле не валидируется и не попадает в ответы — иначе обязательная
|
|
46
47
|
* «уточните какой квест» блокировала бы отправку у всех остальных.
|
|
47
48
|
*/
|
|
48
|
-
const isVisible = (field, values) => {
|
|
49
|
+
const isVisible = (field, values, fields) => {
|
|
49
50
|
var _a;
|
|
50
|
-
|
|
51
|
-
if (!(condition === null || condition === void 0 ? void 0 : condition.fieldId))
|
|
52
|
-
return true;
|
|
53
|
-
const parentValue = values[condition.fieldId];
|
|
54
|
-
const optionIds = ((_a = condition.optionIds) === null || _a === void 0 ? void 0 : _a.length)
|
|
55
|
-
? condition.optionIds
|
|
56
|
-
: condition.optionId
|
|
57
|
-
? [condition.optionId]
|
|
58
|
-
: [];
|
|
59
|
-
if (!optionIds.length) {
|
|
60
|
-
return Array.isArray(parentValue) ? parentValue.length > 0 : !!parentValue;
|
|
61
|
-
}
|
|
62
|
-
if (Array.isArray(parentValue)) {
|
|
63
|
-
return parentValue.some((value) => optionIds.includes(value));
|
|
64
|
-
}
|
|
65
|
-
return typeof parentValue === 'string' && optionIds.includes(parentValue);
|
|
51
|
+
return (0, lead_form_layout_1.isLeadFieldVisible)(field, fields || [field, { id: (_a = field.conditional) === null || _a === void 0 ? void 0 : _a.fieldId }], (id) => values[id]);
|
|
66
52
|
};
|
|
67
53
|
exports.isVisible = isVisible;
|
|
68
54
|
const buildInitialValues = (form, prefill) => {
|
|
@@ -114,7 +100,7 @@ const validate = ({ form, values, phoneValid, t, fieldIds, requireContact = true
|
|
|
114
100
|
for (const field of form.fields) {
|
|
115
101
|
if (fieldIds && !fieldIds.has(field.id))
|
|
116
102
|
continue;
|
|
117
|
-
if (!(0, exports.isVisible)(field, values))
|
|
103
|
+
if (!(0, exports.isVisible)(field, values, form.fields))
|
|
118
104
|
continue;
|
|
119
105
|
const value = values[field.id];
|
|
120
106
|
const empty = isEmpty(value);
|
|
@@ -153,9 +139,9 @@ exports.validate = validate;
|
|
|
153
139
|
* такого поля может не быть, а менеджеру этот контекст нужен в карточке.
|
|
154
140
|
*/
|
|
155
141
|
const buildAnswers = (form, values, contextNote) => {
|
|
156
|
-
const commentField = form.fields.find((field) => field.mapsTo === lead_field_mapping_enum_1.LeadFieldMappingEnum.COMMENT && (0, exports.isVisible)(field, values));
|
|
142
|
+
const commentField = form.fields.find((field) => field.mapsTo === lead_field_mapping_enum_1.LeadFieldMappingEnum.COMMENT && (0, exports.isVisible)(field, values, form.fields));
|
|
157
143
|
return form.fields
|
|
158
|
-
.filter((field) => (0, exports.isVisible)(field, values))
|
|
144
|
+
.filter((field) => (0, exports.isVisible)(field, values, form.fields))
|
|
159
145
|
.map((field) => {
|
|
160
146
|
const value = normalizeValue(values[field.id]);
|
|
161
147
|
if (!contextNote || field.id !== (commentField === null || commentField === void 0 ? void 0 : commentField.id)) {
|
|
@@ -67,3 +67,21 @@ describe('lead form step validation', () => {
|
|
|
67
67
|
expect((0, lib_1.buildInitialValues)(guestsForm)).toEqual({ guests: '' });
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
|
+
describe('conditional chains', () => {
|
|
71
|
+
it('excludes a required descendant when its parent is hidden despite stale answers', () => {
|
|
72
|
+
const fields = [
|
|
73
|
+
Object.assign(Object.assign({}, form.fields[0]), { id: 'audience', required: false }),
|
|
74
|
+
Object.assign(Object.assign({}, form.fields[0]), { id: 'interest', required: false, conditional: { fieldId: 'audience', optionId: 'children' } }),
|
|
75
|
+
Object.assign(Object.assign({}, form.fields[0]), { id: 'detail', conditional: { fieldId: 'interest', optionId: 'magic' } }),
|
|
76
|
+
];
|
|
77
|
+
const values = { audience: 'adults', interest: 'magic', detail: '' };
|
|
78
|
+
expect((0, lib_1.isVisible)(fields[2], values, fields)).toBe(false);
|
|
79
|
+
expect((0, lib_1.validate)({
|
|
80
|
+
form: Object.assign(Object.assign({}, form), { fields }),
|
|
81
|
+
values,
|
|
82
|
+
phoneValid: false,
|
|
83
|
+
t: (key) => key,
|
|
84
|
+
requireContact: false,
|
|
85
|
+
}).errors).toEqual({});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -10,6 +10,8 @@ const react_i18next_1 = require("react-i18next");
|
|
|
10
10
|
* в витрине пакетов (`use-package-t`).
|
|
11
11
|
*/
|
|
12
12
|
const DEFAULTS = {
|
|
13
|
+
'lead.customStepCounter': 'Step {{current}} of {{total}}',
|
|
14
|
+
'lead.continue': 'Continue',
|
|
13
15
|
'lead.title': 'Leave a request',
|
|
14
16
|
'lead.hint': 'Tell us what you need and we will call you back.',
|
|
15
17
|
'lead.submit': 'Send',
|
|
@@ -56,6 +58,8 @@ const DEFAULTS = {
|
|
|
56
58
|
};
|
|
57
59
|
/** Новые ключи, которых ещё нет в сервисе: русский аудитории показываем сразу. */
|
|
58
60
|
const RU_DEFAULTS = {
|
|
61
|
+
'lead.customStepCounter': 'Шаг {{current}} из {{total}}',
|
|
62
|
+
'lead.continue': 'Далее',
|
|
59
63
|
'lead.footnote': 'Менеджер свяжется с вами и предложит варианты. Заявка пока не бронирует игру.',
|
|
60
64
|
'lead.timeUndecided': 'Пока не определился со временем',
|
|
61
65
|
'lead.guestsUnknown': 'Не знаю',
|
|
@@ -78,7 +82,7 @@ const RU_DEFAULTS = {
|
|
|
78
82
|
};
|
|
79
83
|
const useLeadT = () => {
|
|
80
84
|
var _a;
|
|
81
|
-
const { t, i18n } = (0, react_i18next_1.useTranslation)();
|
|
85
|
+
const { t, i18n } = (0, react_i18next_1.useTranslation)('widgets');
|
|
82
86
|
const isRuLang = (_a = i18n.language) === null || _a === void 0 ? void 0 : _a.startsWith('ru');
|
|
83
87
|
return (0, react_1.useCallback)((key, options) => t(key, Object.assign({ defaultValue: (isRuLang && RU_DEFAULTS[key]) || DEFAULTS[key] || key }, options)), [t, isRuLang]);
|
|
84
88
|
};
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export type LocalstoreSetValue<T> = T | undefined | ((prev: T | undefined) => T | undefined);
|
|
2
|
+
export declare function readFromLocalstore<T>(token: string, defaultValue?: T): T | undefined;
|
|
3
|
+
export declare function writeToLocalstore<T>(token: string, value: T | undefined): void;
|
|
2
4
|
export declare function useLocalstoreState<T>(token: string, defaultValue?: T): [T | undefined, (update: LocalstoreSetValue<T>) => void];
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readFromLocalstore = readFromLocalstore;
|
|
4
|
+
exports.writeToLocalstore = writeToLocalstore;
|
|
3
5
|
exports.useLocalstoreState = useLocalstoreState;
|
|
4
6
|
const react_1 = require("react");
|
|
5
7
|
function readFromLocalstore(token, defaultValue) {
|
|
8
|
+
var _a;
|
|
6
9
|
if (typeof window === 'undefined') {
|
|
7
10
|
return defaultValue;
|
|
8
11
|
}
|
|
@@ -12,11 +15,32 @@ function readFromLocalstore(token, defaultValue) {
|
|
|
12
15
|
return JSON.parse(prev);
|
|
13
16
|
}
|
|
14
17
|
}
|
|
15
|
-
catch (
|
|
16
|
-
|
|
18
|
+
catch (_b) {
|
|
19
|
+
try {
|
|
20
|
+
(_a = window.localStorage) === null || _a === void 0 ? void 0 : _a.removeItem(token);
|
|
21
|
+
}
|
|
22
|
+
catch (_c) {
|
|
23
|
+
// Storage itself can be blocked, not just contain invalid JSON.
|
|
24
|
+
}
|
|
17
25
|
}
|
|
18
26
|
return defaultValue;
|
|
19
27
|
}
|
|
28
|
+
function writeToLocalstore(token, value) {
|
|
29
|
+
var _a, _b;
|
|
30
|
+
if (typeof window === 'undefined')
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
if (value) {
|
|
34
|
+
(_a = window.localStorage) === null || _a === void 0 ? void 0 : _a.setItem(token, JSON.stringify(value));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
(_b = window.localStorage) === null || _b === void 0 ? void 0 : _b.removeItem(token);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (_c) {
|
|
41
|
+
// Keep React state usable when storage is unavailable or its quota is full.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
20
44
|
function useLocalstoreState(token, defaultValue) {
|
|
21
45
|
// We track defaultValue via ref so token changes don't pull in stale closures
|
|
22
46
|
// while also avoiding extra effect runs when consumers pass an inline default.
|
|
@@ -41,12 +65,7 @@ function useLocalstoreState(token, defaultValue) {
|
|
|
41
65
|
if (next === prev) {
|
|
42
66
|
return prev;
|
|
43
67
|
}
|
|
44
|
-
|
|
45
|
-
localStorage.setItem(token, JSON.stringify(next));
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
localStorage.removeItem(token);
|
|
49
|
-
}
|
|
68
|
+
writeToLocalstore(token, next);
|
|
50
69
|
return next;
|
|
51
70
|
});
|
|
52
71
|
}, [token]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const index_1 = require("./index");
|
|
4
|
+
describe('unavailable local storage', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
delete globalThis.window;
|
|
7
|
+
delete globalThis.localStorage;
|
|
8
|
+
});
|
|
9
|
+
it.each(['null', 'blocked'])('returns a default and allows writes with %s storage', (mode) => {
|
|
10
|
+
const getStorage = () => {
|
|
11
|
+
if (mode === 'blocked')
|
|
12
|
+
throw new Error('Access denied');
|
|
13
|
+
return null;
|
|
14
|
+
};
|
|
15
|
+
const browser = {};
|
|
16
|
+
Object.defineProperty(browser, 'localStorage', { get: getStorage });
|
|
17
|
+
globalThis.window = browser;
|
|
18
|
+
Object.defineProperty(globalThis, 'localStorage', { get: getStorage, configurable: true });
|
|
19
|
+
expect((0, index_1.readFromLocalstore)('client', 'default')).toBe('default');
|
|
20
|
+
expect(() => (0, index_1.writeToLocalstore)('client', undefined)).not.toThrow();
|
|
21
|
+
expect(() => (0, index_1.writeToLocalstore)('client', 'new value')).not.toThrow();
|
|
22
|
+
});
|
|
23
|
+
it('still removes malformed JSON when storage works', () => {
|
|
24
|
+
const storage = { getItem: () => '{', removeItem: jest.fn() };
|
|
25
|
+
globalThis.window = { localStorage: storage };
|
|
26
|
+
globalThis.localStorage = storage;
|
|
27
|
+
expect((0, index_1.readFromLocalstore)('client', 'default')).toBe('default');
|
|
28
|
+
expect(storage.removeItem).toHaveBeenCalledWith('client');
|
|
29
|
+
});
|
|
30
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escapenavigator/hooks",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.10",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"test": "jest"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@escapenavigator/services": "^2.2.
|
|
18
|
-
"@escapenavigator/types": "^2.2.
|
|
19
|
-
"@escapenavigator/utils": "^2.2.
|
|
17
|
+
"@escapenavigator/services": "^2.2.9",
|
|
18
|
+
"@escapenavigator/types": "^2.2.8",
|
|
19
|
+
"@escapenavigator/utils": "^2.2.8",
|
|
20
20
|
"libphonenumber-js": "^1.12.24",
|
|
21
21
|
"react": "19.2.6",
|
|
22
22
|
"react-phone-input-2": "^2.15.1"
|
|
@@ -32,5 +32,5 @@
|
|
|
32
32
|
"ts-jest": "^29.1.1",
|
|
33
33
|
"typescript": "^5.6"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "30230db507f7483ba425b0811f31d980d3b0a684"
|
|
36
36
|
}
|