@capillarytech/creatives-library 9.0.56 → 9.0.57-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants/unified.js +3 -2
- package/package.json +1 -1
- package/utils/common.js +8 -0
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +334 -46
- package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +10 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +74 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +74 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +28 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +21 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +45 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +162 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +37 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +70 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +274 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +250 -0
- package/v2Components/FormBuilder/Functional/channels/registry.js +2 -0
- package/v2Components/FormBuilder/Functional/constants.js +43 -8
- package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +175 -28
- package/v2Components/FormBuilder/Functional/core/store/formReducer.js +75 -6
- package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +10 -20
- package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +9 -1
- package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +20 -3
- package/v2Components/FormBuilder/Functional/layout/Section.js +6 -1
- package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +89 -0
- package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +447 -0
- package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +9 -2
- package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +430 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +313 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +141 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +306 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +164 -0
- package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +368 -0
- package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +17 -0
- package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +110 -0
- package/v2Components/FormBuilder/_formBuilder.scss +8 -0
- package/v2Components/FormBuilder/index.js +34 -12
- package/v2Components/FormBuilder/tests/entryGate.test.js +94 -7
- package/v2Components/FormBuilder/tests/mpush.characterization.test.js +459 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MOBILEPUSH field renderers — presentational mirrors of Classic's render cases.
|
|
3
|
+
* Prop contract: { field, value, error, resolvedError, onChange, onBlur, onEvent, renderContext }.
|
|
4
|
+
* ButtonField/TagListField are reused from the SMS set; the rest are MPUSH variants
|
|
5
|
+
* where Classic's behavior differs by channel (each notes its Classic lines).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import PropTypes from 'prop-types';
|
|
10
|
+
import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
|
|
11
|
+
import CapInput from '@capillarytech/cap-ui-library/CapInput';
|
|
12
|
+
import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
|
|
13
|
+
import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
|
|
14
|
+
import CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
15
|
+
import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
|
|
16
|
+
import CapImage from '@capillarytech/cap-ui-library/CapImage';
|
|
17
|
+
import CapRadio from '@capillarytech/cap-ui-library/CapRadio';
|
|
18
|
+
import CapRadioGroup from '@capillarytech/cap-ui-library/CapRadioGroup';
|
|
19
|
+
import CapSelect from '@capillarytech/cap-ui-library/CapSelect';
|
|
20
|
+
import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
|
|
21
|
+
import LabelHOC from '@capillarytech/cap-ui-library/assets/HOCs/ComponentWithLabelHOC';
|
|
22
|
+
import { FONT_COLOR_04, FONT_COLOR_05 } from '@capillarytech/cap-ui-library/styled/variables';
|
|
23
|
+
import UnifiedPreview from '../../../CommonTestAndPreview/UnifiedPreview';
|
|
24
|
+
import { ANDROID } from '../../../CommonTestAndPreview/constants';
|
|
25
|
+
import { MOBILE_PUSH } from '../../../../v2Containers/CreativesContainer/constants';
|
|
26
|
+
import globalMessages from '../../../../v2Containers/Cap/messages';
|
|
27
|
+
import formMessages from '../../messages';
|
|
28
|
+
import { isAiContentBotDisabled } from '../../../../utils/common';
|
|
29
|
+
import { hasPersonalizationTags } from '../../../../utils/commonUtils';
|
|
30
|
+
import { createFieldRegistry } from '../core/schema/fieldRegistry';
|
|
31
|
+
import { FIELD_TYPE } from '../constants';
|
|
32
|
+
import { ButtonField, TagListField } from './smsRenderers';
|
|
33
|
+
import { ERROR_VALUE } from '../channels/mobilepush/config';
|
|
34
|
+
|
|
35
|
+
const { TextArea } = CapInput;
|
|
36
|
+
|
|
37
|
+
// Image types the upload accepts (Classic.js:2746 wrong-file check).
|
|
38
|
+
const ALLOWED_IMAGE_EXTENSIONS = /(\.bmp|\.jpeg|\.png|\.gif|\.avif|\.jpg)$/i;
|
|
39
|
+
|
|
40
|
+
const dataFieldPropTypes = {
|
|
41
|
+
field: PropTypes.object.isRequired,
|
|
42
|
+
value: PropTypes.any,
|
|
43
|
+
error: PropTypes.any,
|
|
44
|
+
onChange: PropTypes.func.isRequired,
|
|
45
|
+
renderContext: PropTypes.object.isRequired,
|
|
46
|
+
};
|
|
47
|
+
const dataFieldDefaultProps = {
|
|
48
|
+
value: undefined,
|
|
49
|
+
error: undefined,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const formatSafe = (intl, descriptor) => (intl?.formatMessage ? intl.formatMessage(descriptor) : '');
|
|
53
|
+
|
|
54
|
+
// Classic gates every field-error display on checkValidation (pre-save silence).
|
|
55
|
+
const shouldShowError = (renderContext, error) => Boolean(renderContext?.checkValidation && error);
|
|
56
|
+
|
|
57
|
+
/** Classic input-case error message: personalization > brace > schema errorMessage. */
|
|
58
|
+
const resolveInputMessage = (field, value, error, renderContext) => {
|
|
59
|
+
const { intl, restrictPersonalization } = renderContext || {};
|
|
60
|
+
if (restrictPersonalization && value && hasPersonalizationTags(String(value))) {
|
|
61
|
+
return formatSafe(intl, formMessages.personalizationTagsErrorMessage);
|
|
62
|
+
}
|
|
63
|
+
if (error === ERROR_VALUE.BRACKET) {
|
|
64
|
+
return formatSafe(intl, globalMessages.unbalanacedCurlyBraces);
|
|
65
|
+
}
|
|
66
|
+
return field.errorMessage || '';
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const MpushInputField = ({
|
|
70
|
+
field, value, error, onChange, onBlur, renderContext,
|
|
71
|
+
}) => {
|
|
72
|
+
const showError = shouldShowError(renderContext, error);
|
|
73
|
+
const message = showError ? resolveInputMessage(field, value, error, renderContext) : '';
|
|
74
|
+
return (
|
|
75
|
+
<CapColumn key={field.id} span={field.width} offset={field.offset} style={field.style || {}}>
|
|
76
|
+
<CapInput
|
|
77
|
+
id={field.id}
|
|
78
|
+
label={field.label}
|
|
79
|
+
placeholder={field.placeholder}
|
|
80
|
+
errorMessage={message}
|
|
81
|
+
className={`input-primary chart-name-input${showError ? ' error' : ''}`}
|
|
82
|
+
value={value || ''}
|
|
83
|
+
onChange={(e) => onChange(e.target.value)}
|
|
84
|
+
onBlur={onBlur}
|
|
85
|
+
disabled={field.disabled}
|
|
86
|
+
size={field.size || 'default'}
|
|
87
|
+
/>
|
|
88
|
+
</CapColumn>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
MpushInputField.propTypes = { ...dataFieldPropTypes, onBlur: PropTypes.func };
|
|
93
|
+
MpushInputField.defaultProps = { ...dataFieldDefaultProps, onBlur: undefined };
|
|
94
|
+
|
|
95
|
+
export const MpushTextAreaField = ({
|
|
96
|
+
field, value, error, onChange, renderContext,
|
|
97
|
+
}) => {
|
|
98
|
+
let aiDisabled = true;
|
|
99
|
+
try { aiDisabled = isAiContentBotDisabled(); } catch (e) { aiDisabled = true; }
|
|
100
|
+
// Classic-observable behavior: errorData is only ever computed under
|
|
101
|
+
// startValidation (Classic.js:334-345), so NO editor error — empty or tag —
|
|
102
|
+
// is visible before a save attempt. checkValidation is true exactly in the
|
|
103
|
+
// states where Classic can hold a non-false errorType, so gating display on it
|
|
104
|
+
// reproduces Classic's typing silence and also keeps the initial-hydrate
|
|
105
|
+
// errorData (computed for the parent validity emission) from leaking into the UI.
|
|
106
|
+
const showError = shouldShowError(renderContext, error);
|
|
107
|
+
// MPUSH keeps the inline message even though the channel is liquid-supported
|
|
108
|
+
// (Classic.js:2908-2915). Personalization gets its own message (2877-2879).
|
|
109
|
+
const inlineMessage = showError ? resolveInputMessage(field, value, error, renderContext) : '';
|
|
110
|
+
return (
|
|
111
|
+
<CapColumn key={field.id} span={field.width} offset={field.offset}>
|
|
112
|
+
<TextArea
|
|
113
|
+
id={field.id}
|
|
114
|
+
label={field.label}
|
|
115
|
+
placeholder={field.placeholder || ''}
|
|
116
|
+
className={`${showError ? 'error-form-builder' : ''}`}
|
|
117
|
+
errorMessage={inlineMessage}
|
|
118
|
+
autosize={field.autosize ? field.autosizeParams : false}
|
|
119
|
+
value={value || ''}
|
|
120
|
+
onChange={(e) => onChange(e.target.value)}
|
|
121
|
+
style={field.style || {}}
|
|
122
|
+
disabled={field.disabled}
|
|
123
|
+
/>
|
|
124
|
+
{!aiDisabled && (
|
|
125
|
+
// AskAira content bot is enabled for MOBILEPUSH textareas (Classic.js:2927).
|
|
126
|
+
<CapAskAira.ContentGenerationBot
|
|
127
|
+
text={value || ''}
|
|
128
|
+
setText={(x) => onChange(x)}
|
|
129
|
+
iconPlacement="float-br"
|
|
130
|
+
rootStyle={{ bottom: 'calc(1rem + 0.2rem)', right: '0.2rem' }}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
</CapColumn>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
MpushTextAreaField.propTypes = { ...dataFieldPropTypes };
|
|
138
|
+
MpushTextAreaField.defaultProps = { ...dataFieldDefaultProps };
|
|
139
|
+
|
|
140
|
+
export const MpushCheckboxField = ({
|
|
141
|
+
field, value, error, onChange, onEvent, renderContext,
|
|
142
|
+
}) => {
|
|
143
|
+
const showError = shouldShowError(renderContext, error);
|
|
144
|
+
const checkbox = (
|
|
145
|
+
<CapCheckbox
|
|
146
|
+
key={field.id}
|
|
147
|
+
className={`${showError ? 'error' : ''}`}
|
|
148
|
+
errorMessage={field.errorMessage && showError ? field.errorMessage : ''}
|
|
149
|
+
onChange={(e) => {
|
|
150
|
+
const { checked } = e.target;
|
|
151
|
+
onChange(checked);
|
|
152
|
+
// CTA toggles route through the injected submit-action with the
|
|
153
|
+
// performFormDataUpdate commit signature (Classic.js:3567 -> 228).
|
|
154
|
+
if (field.submitAction && onEvent) onEvent(field.submitAction, checked);
|
|
155
|
+
}}
|
|
156
|
+
checked={Boolean(value)}
|
|
157
|
+
style={field.style || {}}
|
|
158
|
+
disabled={field.disabled}
|
|
159
|
+
inductiveText={field.inductiveText}
|
|
160
|
+
>
|
|
161
|
+
{field.label}
|
|
162
|
+
</CapCheckbox>
|
|
163
|
+
);
|
|
164
|
+
return (
|
|
165
|
+
<CapColumn key={field.id} span={field.width} offset={field.offset}>
|
|
166
|
+
{field.disabled && field.hoverText
|
|
167
|
+
? <CapTooltip title={field.hoverText}>{checkbox}</CapTooltip>
|
|
168
|
+
: checkbox}
|
|
169
|
+
</CapColumn>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
MpushCheckboxField.propTypes = { ...dataFieldPropTypes, onEvent: PropTypes.func };
|
|
174
|
+
MpushCheckboxField.defaultProps = { ...dataFieldDefaultProps, onEvent: undefined };
|
|
175
|
+
|
|
176
|
+
// Direct renderer (needs the full legacy formData for the value-resolution chain).
|
|
177
|
+
export const RadioGroupField = ({
|
|
178
|
+
field, error, onChange, onEvent, renderContext,
|
|
179
|
+
}) => {
|
|
180
|
+
const showError = shouldShowError(renderContext, error);
|
|
181
|
+
// Classic handleSetRadioValue (2729-2736): tab value -> root value -> schema default.
|
|
182
|
+
const legacy = renderContext?.legacyFormData || {};
|
|
183
|
+
const tab = legacy[(renderContext?.paneTabIndex ?? renderContext?.activeTabIndex) || 0] || {};
|
|
184
|
+
const resolvedValue = tab[field.id] !== undefined && tab[field.id] !== ''
|
|
185
|
+
? tab[field.id]
|
|
186
|
+
: (legacy[field.id] !== undefined && legacy[field.id] !== '' ? legacy[field.id] : field.value);
|
|
187
|
+
return (
|
|
188
|
+
<CapColumn key={`input-${field.id}`} span={field.width} offset={field.offset}>
|
|
189
|
+
<CapRadioGroup
|
|
190
|
+
key={`${field.id}-radio-group`}
|
|
191
|
+
className={`form-builder-radio-group ${showError ? 'error' : ''}`}
|
|
192
|
+
errorMessage={field.errorMessage && showError ? field.errorMessage : ''}
|
|
193
|
+
onChange={(e) => {
|
|
194
|
+
const nextValue = e.target.value;
|
|
195
|
+
onChange(nextValue);
|
|
196
|
+
onEvent('onChange', nextValue); // -> injected onLinkTypeChange (commit signature)
|
|
197
|
+
}}
|
|
198
|
+
style={field.style || {}}
|
|
199
|
+
value={resolvedValue}
|
|
200
|
+
name={field.name || `${field.id}-name`}
|
|
201
|
+
disabled={field.disabled}
|
|
202
|
+
>
|
|
203
|
+
{(field.options || []).map((option, index) => (
|
|
204
|
+
<CapRadio
|
|
205
|
+
key={`${field.id}-radio-${index}`}
|
|
206
|
+
value={option}
|
|
207
|
+
inductiveText={field.inductiveText && field.inductiveText[index]}
|
|
208
|
+
>
|
|
209
|
+
{option}
|
|
210
|
+
</CapRadio>
|
|
211
|
+
))}
|
|
212
|
+
</CapRadioGroup>
|
|
213
|
+
</CapColumn>
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
RadioGroupField.propTypes = { ...dataFieldPropTypes, onEvent: PropTypes.func.isRequired };
|
|
218
|
+
RadioGroupField.defaultProps = { ...dataFieldDefaultProps };
|
|
219
|
+
|
|
220
|
+
// Direct renderer (options may be overridden through `${id}-options` in the form data).
|
|
221
|
+
export const SelectField = ({
|
|
222
|
+
field, value, error, onChange, onEvent, renderContext,
|
|
223
|
+
}) => {
|
|
224
|
+
const showError = shouldShowError(renderContext, error);
|
|
225
|
+
const options = renderContext?.legacyFormData?.[`${field.id}-options`] || field.options;
|
|
226
|
+
return (
|
|
227
|
+
<CapColumn key={`select-${field.id}`} style={field.colStyle || {}} span={field.width} offset={field.offset}>
|
|
228
|
+
<CapSelect
|
|
229
|
+
id={field.id}
|
|
230
|
+
options={options}
|
|
231
|
+
placeholder={field.placeholder || ''}
|
|
232
|
+
style={field.style || {}}
|
|
233
|
+
onSelect={(data) => {
|
|
234
|
+
onChange(data);
|
|
235
|
+
onEvent('onSelect', data); // -> injected showCtaKeys / onTemplateChange (commit signature)
|
|
236
|
+
}}
|
|
237
|
+
value={value}
|
|
238
|
+
disabled={field.disabled}
|
|
239
|
+
label={field.label}
|
|
240
|
+
/>
|
|
241
|
+
{showError && field.errorMessage && <span className="error">{field.errorMessage}</span>}
|
|
242
|
+
</CapColumn>
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
SelectField.propTypes = { ...dataFieldPropTypes, onEvent: PropTypes.func.isRequired };
|
|
247
|
+
SelectField.defaultProps = { ...dataFieldDefaultProps };
|
|
248
|
+
|
|
249
|
+
// Image upload (imageSchema only), Classic.js:3657-3690 + 2737-2764: the 5 MB check
|
|
250
|
+
// and dimension read happen here; the upload + formData write stay container-owned.
|
|
251
|
+
export const UploadField = ({
|
|
252
|
+
field, value, error, onEvent, renderContext,
|
|
253
|
+
}) => {
|
|
254
|
+
// The only field whose error display also honors startValidation (Classic.js:3666).
|
|
255
|
+
const showError = Boolean(
|
|
256
|
+
(renderContext?.checkValidation || renderContext?.startValidation) && error,
|
|
257
|
+
);
|
|
258
|
+
const isImage = Boolean(value);
|
|
259
|
+
|
|
260
|
+
const openFileDialog = (e) => {
|
|
261
|
+
if (e) e.preventDefault();
|
|
262
|
+
const fileInput = document.querySelector(`#${field.id} #fileName`);
|
|
263
|
+
if (fileInput) fileInput.click();
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const onFileChange = (e) => {
|
|
267
|
+
const files = e?.target?.files;
|
|
268
|
+
const file = files && files[0];
|
|
269
|
+
if (!file) return;
|
|
270
|
+
if (field.supportedExtensions && !ALLOWED_IMAGE_EXTENSIONS.exec(file.name)) {
|
|
271
|
+
// Classic fires the wrong-file event and CONTINUES (no early return) — preserved.
|
|
272
|
+
onEvent(field.submitAction, { file, type: 'wrong file' });
|
|
273
|
+
}
|
|
274
|
+
const objectUrl = (window.URL || window.webkitURL).createObjectURL(file);
|
|
275
|
+
const img = new Image();
|
|
276
|
+
img.src = objectUrl;
|
|
277
|
+
img.onload = () => {
|
|
278
|
+
const fileParams = {
|
|
279
|
+
width: img.width,
|
|
280
|
+
height: img.height,
|
|
281
|
+
error: Boolean(file && (file.size / 1e6 > 5)), // > 5 MB (Classic.js:2753)
|
|
282
|
+
};
|
|
283
|
+
onEvent(field.submitAction, { file, type: 'image', fileParams });
|
|
284
|
+
};
|
|
285
|
+
e.target.value = null;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const ImageComponent = (imageProps) => (
|
|
289
|
+
<div key={`${field.id}-preview`}>
|
|
290
|
+
<div className={`image-container ${imageProps.ifError ? 'error' : ''}`}>
|
|
291
|
+
{isImage
|
|
292
|
+
? <CapImage src={value} alt={imageProps.alt} style={imageProps.style} />
|
|
293
|
+
: (
|
|
294
|
+
<div className="fb-upload-placeholder">
|
|
295
|
+
<span className="image-placeholder">{imageProps.placeholder}</span>
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
const WithLabel = LabelHOC(ImageComponent);
|
|
302
|
+
const { errorMessage, ...previewRest } = field.previewProps || {};
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<CapColumn span={field.width} offset={field.offset} style={field.style}>
|
|
306
|
+
{field.showPreview && (
|
|
307
|
+
<WithLabel
|
|
308
|
+
key={`${field.id}-with-label`}
|
|
309
|
+
{...previewRest}
|
|
310
|
+
errorMessage={showError && errorMessage}
|
|
311
|
+
ifError={showError}
|
|
312
|
+
/>
|
|
313
|
+
)}
|
|
314
|
+
<form encType="multipart/form-data" id={field.id}>
|
|
315
|
+
{/* Native file input on purpose — openFileDialog clicks it via #fileName;
|
|
316
|
+
CapInput wraps antd's text Input (affix wrapper + suffix), which cannot
|
|
317
|
+
host type="file". Hidden: the visible control is the CapButton below. */}
|
|
318
|
+
<input
|
|
319
|
+
key={field.id}
|
|
320
|
+
className="fb-upload-file-input"
|
|
321
|
+
id="fileName"
|
|
322
|
+
type="file"
|
|
323
|
+
onChange={onFileChange}
|
|
324
|
+
accept={field.supportedExtensions ? field.supportedExtensions : 'image/*'}
|
|
325
|
+
/>
|
|
326
|
+
<CapButton
|
|
327
|
+
disabled={field.disabled || false}
|
|
328
|
+
type="link"
|
|
329
|
+
prefix={<CapIcon size="s" type="add-photo" />}
|
|
330
|
+
onClick={openFileDialog}
|
|
331
|
+
// color is state-dependent and must beat CapButton's own runtime styling —
|
|
332
|
+
// kept inline (Classic.js:3399 verbatim).
|
|
333
|
+
style={{ float: 'right', color: `${field.disabled ? FONT_COLOR_04 : FONT_COLOR_05}` }}
|
|
334
|
+
>
|
|
335
|
+
{field.label}
|
|
336
|
+
</CapButton>
|
|
337
|
+
</form>
|
|
338
|
+
</CapColumn>
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
UploadField.propTypes = { ...dataFieldPropTypes, onEvent: PropTypes.func.isRequired };
|
|
343
|
+
UploadField.defaultProps = { ...dataFieldDefaultProps };
|
|
344
|
+
|
|
345
|
+
// Primitive div (tab headers, copy links, key labels), Classic.js:3304-3328:
|
|
346
|
+
// value resolves from the ACTIVE tab; DOM id gets the legacy pane suffix (`${id}2`).
|
|
347
|
+
export const MpushDivField = ({ field, onEvent, renderContext }) => {
|
|
348
|
+
const paneTabIndex = renderContext?.paneTabIndex ?? renderContext?.activeTabIndex ?? 0;
|
|
349
|
+
const activeTab = renderContext?.legacyFormData?.[renderContext?.activeTabIndex ?? 0] || {};
|
|
350
|
+
const children = activeTab[field.id] || field.value || '';
|
|
351
|
+
const domId = `${field.id}${paneTabIndex > 0 ? paneTabIndex + 1 : ''}`;
|
|
352
|
+
return (
|
|
353
|
+
<div
|
|
354
|
+
className={field.className || ''}
|
|
355
|
+
id={domId}
|
|
356
|
+
onClick={(data) => field.submitAction && onEvent(field.submitAction, data)}
|
|
357
|
+
style={field.style || {}}
|
|
358
|
+
// Classic fires the injected 'onChange' via callChildEvent's TAIL signature
|
|
359
|
+
// (data, field.id) — the shell's commit-signature branch is type-guarded to
|
|
360
|
+
// checkbox/radioGroup/select, so a div's onChange takes the tail path.
|
|
361
|
+
onInput={(e) => { e.stopPropagation(); onEvent('onChange', e.target.textContent); }}
|
|
362
|
+
>
|
|
363
|
+
{children}
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
MpushDivField.propTypes = {
|
|
369
|
+
field: PropTypes.object.isRequired,
|
|
370
|
+
onEvent: PropTypes.func.isRequired,
|
|
371
|
+
renderContext: PropTypes.object.isRequired,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Direct renderer — CTA delete icons.
|
|
375
|
+
export const IconField = ({ field, onEvent }) => (
|
|
376
|
+
<CapColumn
|
|
377
|
+
key={`icon-${field.id}`}
|
|
378
|
+
id={field.id}
|
|
379
|
+
offset={field.offset}
|
|
380
|
+
width={field.width || 1}
|
|
381
|
+
onClick={(data) => field.submitAction && onEvent(field.submitAction, data)}
|
|
382
|
+
className={field.className || ''}
|
|
383
|
+
style={field.colStyle || {}}
|
|
384
|
+
>
|
|
385
|
+
<i style={field.style || {}} className="material-icons">{field.value || ''}</i>
|
|
386
|
+
</CapColumn>
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
IconField.propTypes = {
|
|
390
|
+
field: PropTypes.object.isRequired,
|
|
391
|
+
onEvent: PropTypes.func.isRequired,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// Device preview, Classic.js:3897-3943 quirks preserved: both panes show the ACTIVE
|
|
395
|
+
// tab's content and the frame is hardcoded to ANDROID with the toggle hidden.
|
|
396
|
+
export const MobilePushPreviewField = ({ field, renderContext }) => {
|
|
397
|
+
const activeTab = renderContext?.legacyFormData?.[renderContext?.activeTabIndex ?? 0];
|
|
398
|
+
if (!activeTab) return null;
|
|
399
|
+
const header = activeTab[field.content?.title] || '';
|
|
400
|
+
const bodyText = activeTab[field.content?.message] || '';
|
|
401
|
+
const bodyImage = activeTab.image || '';
|
|
402
|
+
const actions = [];
|
|
403
|
+
if (activeTab[field.content?.secondaryCta1]) actions.push({ label: activeTab[field.content.secondaryCta1] });
|
|
404
|
+
if (activeTab[field.content?.secondaryCta2]) actions.push({ label: activeTab[field.content.secondaryCta2] });
|
|
405
|
+
const platformContent = {
|
|
406
|
+
header, bodyText, bodyImage, actions, appName: field.content?.appName || '',
|
|
407
|
+
};
|
|
408
|
+
const mobilePushContent = { androidContent: platformContent, iosContent: platformContent };
|
|
409
|
+
return (
|
|
410
|
+
<CapColumn key="input" span={23} offset={1}>
|
|
411
|
+
<UnifiedPreview
|
|
412
|
+
key={field.id}
|
|
413
|
+
style={field.customStyling || {}}
|
|
414
|
+
channel={field.channel || MOBILE_PUSH}
|
|
415
|
+
content={mobilePushContent}
|
|
416
|
+
device={ANDROID}
|
|
417
|
+
showDeviceToggle={false}
|
|
418
|
+
showHeader={false}
|
|
419
|
+
formatMessage={renderContext?.intl?.formatMessage}
|
|
420
|
+
/>
|
|
421
|
+
</CapColumn>
|
|
422
|
+
);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
MobilePushPreviewField.propTypes = {
|
|
426
|
+
field: PropTypes.object.isRequired,
|
|
427
|
+
renderContext: PropTypes.object.isRequired,
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
/** Build a registry pre-loaded with the MOBILEPUSH field renderers. */
|
|
431
|
+
export const createMpushRegistry = () => createFieldRegistry({
|
|
432
|
+
[FIELD_TYPE.INPUT]: MpushInputField,
|
|
433
|
+
[FIELD_TYPE.TEXTAREA]: MpushTextAreaField,
|
|
434
|
+
[FIELD_TYPE.CHECKBOX]: MpushCheckboxField,
|
|
435
|
+
[FIELD_TYPE.BUTTON]: ButtonField,
|
|
436
|
+
// TagListField mirrors Classic's SEMANTIC tag-list path (Classic.js:3730-3769):
|
|
437
|
+
// span = field.width || '', field.style applied — the CTA splices rely on it.
|
|
438
|
+
[FIELD_TYPE.TAG_LIST]: TagListField,
|
|
439
|
+
[FIELD_TYPE.RADIO_GROUP]: RadioGroupField,
|
|
440
|
+
[FIELD_TYPE.SELECT]: SelectField,
|
|
441
|
+
[FIELD_TYPE.UPLOAD]: UploadField,
|
|
442
|
+
[FIELD_TYPE.DIV]: MpushDivField,
|
|
443
|
+
[FIELD_TYPE.ICON]: IconField,
|
|
444
|
+
[FIELD_TYPE.MOBILE_PUSH_PREVIEW]: MobilePushPreviewField,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
export default createMpushRegistry;
|
|
@@ -54,7 +54,7 @@ describe('FieldSlot memoization', () => {
|
|
|
54
54
|
expect(Renderer).toHaveBeenCalledTimes(2);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
it('passes a minimal renderContext (
|
|
57
|
+
it('passes a minimal renderContext (checkValidation/channel + memo-safe intl/restrictPersonalization) and wires onChange to its field', () => {
|
|
58
58
|
const onFieldChange = jest.fn();
|
|
59
59
|
let captured;
|
|
60
60
|
const Renderer = jest.fn((p) => { captured = p; return <div>f</div>; });
|
|
@@ -71,7 +71,14 @@ describe('FieldSlot memoization', () => {
|
|
|
71
71
|
onEvent={noop}
|
|
72
72
|
/>,
|
|
73
73
|
);
|
|
74
|
-
|
|
74
|
+
// intl + restrictPersonalization joined the mini-context in Phase 2 (MPUSH):
|
|
75
|
+
// both are memo-safe (stable identity / primitive) and default-empty here.
|
|
76
|
+
expect(captured.renderContext).toEqual({
|
|
77
|
+
checkValidation: true,
|
|
78
|
+
channel: 'SMS',
|
|
79
|
+
intl: undefined,
|
|
80
|
+
restrictPersonalization: false,
|
|
81
|
+
});
|
|
75
82
|
captured.onChange('hello');
|
|
76
83
|
expect(onFieldChange).toHaveBeenCalledWith(field, 'hello');
|
|
77
84
|
});
|