@capillarytech/creatives-library 9.0.13-alpha.0 → 9.0.13-alpha.1
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 +0 -3
- package/package.json +1 -1
- package/utils/common.js +0 -8
- package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberCarouselPreviewCards.js +132 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +108 -15
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +141 -1
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_viberCarouselPreviewCards.scss +132 -0
- package/v2Components/CommonTestAndPreview/index.js +244 -26
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/ViberPreviewContent.test.js +364 -0
- package/v2Components/FormBuilder/_formBuilder.scss +0 -8
- package/v2Components/FormBuilder/index.js +4479 -41
- package/v2Containers/Templates/_templates.scss +83 -0
- package/v2Containers/Templates/index.js +90 -10
- package/v2Containers/Viber/constants.js +21 -0
- package/v2Containers/Viber/index.js +719 -49
- package/v2Containers/Viber/index.scss +175 -0
- package/v2Containers/Viber/messages.js +121 -0
- package/v2Containers/Viber/tests/index.test.js +80 -0
- package/v2Components/FormBuilder/Classic.js +0 -4487
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +0 -369
- package/v2Components/FormBuilder/Functional/channels/registry.js +0 -17
- package/v2Components/FormBuilder/Functional/channels/sms/buildSubmitPayload.js +0 -9
- package/v2Components/FormBuilder/Functional/channels/sms/config.js +0 -30
- package/v2Components/FormBuilder/Functional/channels/sms/getEditorErrorDescriptor.js +0 -46
- package/v2Components/FormBuilder/Functional/channels/sms/getLiquidContent.js +0 -13
- package/v2Components/FormBuilder/Functional/channels/sms/index.js +0 -22
- package/v2Components/FormBuilder/Functional/channels/sms/tests/getEditorErrorDescriptor.test.js +0 -52
- package/v2Components/FormBuilder/Functional/channels/sms/tests/getLiquidContent.test.js +0 -25
- package/v2Components/FormBuilder/Functional/channels/sms/tests/validate.test.js +0 -87
- package/v2Components/FormBuilder/Functional/channels/sms/validate.js +0 -89
- package/v2Components/FormBuilder/Functional/constants.js +0 -39
- package/v2Components/FormBuilder/Functional/core/schema/fieldRegistry.js +0 -38
- package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +0 -85
- package/v2Components/FormBuilder/Functional/core/store/formReducer.js +0 -81
- package/v2Components/FormBuilder/Functional/core/store/selectors.js +0 -30
- package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +0 -91
- package/v2Components/FormBuilder/Functional/index.js +0 -26
- package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +0 -59
- package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +0 -32
- package/v2Components/FormBuilder/Functional/layout/Section.js +0 -118
- package/v2Components/FormBuilder/Functional/renderers/smsRenderers.js +0 -265
- package/v2Components/FormBuilder/Functional/tests/channelRegistry.test.js +0 -21
- package/v2Components/FormBuilder/Functional/tests/fieldRegistry.test.js +0 -65
- package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +0 -97
- package/v2Components/FormBuilder/Functional/tests/fixtures/smsParityCases.js +0 -192
- package/v2Components/FormBuilder/Functional/tests/formReducer.test.js +0 -129
- package/v2Components/FormBuilder/Functional/tests/initializeFormState.test.js +0 -132
- package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -40
- package/v2Components/FormBuilder/Functional/tests/section.test.js +0 -99
- package/v2Components/FormBuilder/Functional/tests/selectors.test.js +0 -67
- package/v2Components/FormBuilder/Functional/tests/sms.crossFlowParity.test.js +0 -155
- package/v2Components/FormBuilder/Functional/tests/sms.liquid.test.js +0 -172
- package/v2Components/FormBuilder/Functional/tests/sms.rollout.test.js +0 -122
- package/v2Components/FormBuilder/Functional/tests/sms.shell.parity.test.js +0 -329
- package/v2Components/FormBuilder/Functional/tests/smsRenderers.test.js +0 -162
- package/v2Components/FormBuilder/Functional/tests/toLegacyFormData.test.js +0 -95
- package/v2Components/FormBuilder/tests/__snapshots__/sms.characterization.test.js.snap +0 -114
- package/v2Components/FormBuilder/tests/entryGate.test.js +0 -106
- package/v2Components/FormBuilder/tests/sms.characterization.test.js +0 -336
- package/v2Components/TemplatePreview/coderabbits_comments +0 -171
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Section — channel-agnostic recursive renderer for the schema's structural
|
|
3
|
-
* section types (parent / multicols / col-label). It resolves each leaf field's
|
|
4
|
-
* renderer from the registry and feeds it the field's primitive value + error +
|
|
5
|
-
* handlers (computed here from the normalized state + the legacy errorData).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import React from 'react';
|
|
9
|
-
import PropTypes from 'prop-types';
|
|
10
|
-
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
11
|
-
import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
|
|
12
|
-
import { selectFieldValue } from '../core/store/selectors';
|
|
13
|
-
import { MEMOIZED_TYPES, SECTION_TYPE } from '../constants';
|
|
14
|
-
import FieldSlot from './FieldSlot';
|
|
15
|
-
|
|
16
|
-
const legacyError = (errorData, field, tabIndex) => {
|
|
17
|
-
if (!errorData) return undefined;
|
|
18
|
-
if (tabIndex == null) return errorData[field?.id];
|
|
19
|
-
return errorData[tabIndex]?.[field?.id];
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const renderField = (field, renderContext) => {
|
|
23
|
-
if (!field?.type) return null;
|
|
24
|
-
const Renderer = renderContext?.registry?.resolve(field.type);
|
|
25
|
-
if (!Renderer) return null; // unknown/unsupported type -> nothing (dead branches don't exist here)
|
|
26
|
-
|
|
27
|
-
const tabIndex = field.standalone ? null : renderContext?.activeTabIndex;
|
|
28
|
-
const value = selectFieldValue(renderContext?.state, { fieldId: field?.id, tabIndex });
|
|
29
|
-
const error = legacyError(renderContext?.errorData, field, tabIndex);
|
|
30
|
-
|
|
31
|
-
if (MEMOIZED_TYPES.has(field.type)) {
|
|
32
|
-
return (
|
|
33
|
-
<FieldSlot
|
|
34
|
-
key={field.id}
|
|
35
|
-
Renderer={Renderer}
|
|
36
|
-
field={field}
|
|
37
|
-
value={value}
|
|
38
|
-
error={error}
|
|
39
|
-
resolvedError={renderContext?.resolvedFieldErrors?.[field?.id]}
|
|
40
|
-
checkValidation={renderContext?.checkValidation}
|
|
41
|
-
channel={renderContext?.channel}
|
|
42
|
-
onFieldChange={renderContext?.onFieldChange}
|
|
43
|
-
onFieldBlur={renderContext?.onFieldBlur}
|
|
44
|
-
onEvent={renderContext?.onEvent}
|
|
45
|
-
/>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<Renderer
|
|
51
|
-
key={field.id}
|
|
52
|
-
field={field}
|
|
53
|
-
value={value}
|
|
54
|
-
error={error}
|
|
55
|
-
onChange={(newValue) => renderContext.onFieldChange(field, newValue)}
|
|
56
|
-
onBlur={() => renderContext.onFieldBlur?.(field)}
|
|
57
|
-
onEvent={(eventName, data) => renderContext.onEvent(field, eventName, data)}
|
|
58
|
-
renderContext={renderContext}
|
|
59
|
-
/>
|
|
60
|
-
);
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const Section = ({ section, renderContext }) => {
|
|
64
|
-
if (!section) return null;
|
|
65
|
-
|
|
66
|
-
if (section.type === SECTION_TYPE.PARENT) {
|
|
67
|
-
return (
|
|
68
|
-
<CapRow useLegacy style={section?.rowStyle || {}}>
|
|
69
|
-
{section?.childSections?.map((childSection, index) => (
|
|
70
|
-
// Schema layout is a frozen, never-reordered descriptor -> index is a stable key.
|
|
71
|
-
// eslint-disable-next-line react/no-array-index-key
|
|
72
|
-
<CapColumn key={index} span={childSection?.width} offset={childSection?.offset} style={childSection?.colStyle || {}}>
|
|
73
|
-
<Section section={childSection} renderContext={renderContext} />
|
|
74
|
-
</CapColumn>
|
|
75
|
-
))}
|
|
76
|
-
</CapRow>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (section.type === SECTION_TYPE.MULTICOLS) {
|
|
81
|
-
return (
|
|
82
|
-
<>
|
|
83
|
-
{section?.inputFields?.map((row, index) => (
|
|
84
|
-
<CapRow useLegacy key={`in-${index}`} className={row?.rowClassName} style={row?.rowStyle || {}}>
|
|
85
|
-
{row?.cols?.map((col) => renderField(col, renderContext))}
|
|
86
|
-
</CapRow>
|
|
87
|
-
))}
|
|
88
|
-
{section?.actionFields?.map((row, index) => (
|
|
89
|
-
<CapRow useLegacy key={`act-${index}`} className={row?.rowClassName} style={row?.rowStyle || {}}>
|
|
90
|
-
{row?.cols?.map((col) => renderField(col, renderContext))}
|
|
91
|
-
</CapRow>
|
|
92
|
-
))}
|
|
93
|
-
</>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (section.type === SECTION_TYPE.COL_LABEL) {
|
|
98
|
-
return (
|
|
99
|
-
<>
|
|
100
|
-
{section?.inputFields?.map((field) => renderField(field, renderContext))}
|
|
101
|
-
{section?.actionFields?.map((field) => renderField(field, renderContext))}
|
|
102
|
-
</>
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return null;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
Section.propTypes = {
|
|
110
|
-
section: PropTypes.object,
|
|
111
|
-
renderContext: PropTypes.object.isRequired,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
Section.defaultProps = {
|
|
115
|
-
section: null,
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
export default Section;
|
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SMS field renderers — pure presentational components for the SMS schema field
|
|
3
|
-
* types, reusing the cap-ui-library components for visual parity with the class
|
|
4
|
-
* component. Prop contract: { field, value, error, resolvedError, onChange, onBlur, onEvent, renderContext }.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import React from 'react';
|
|
8
|
-
import PropTypes from 'prop-types';
|
|
9
|
-
import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
|
|
10
|
-
import CapInput from '@capillarytech/cap-ui-library/CapInput';
|
|
11
|
-
import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
|
|
12
|
-
import CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
13
|
-
import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
|
|
14
|
-
import UnifiedPreview from '../../../CommonTestAndPreview/UnifiedPreview';
|
|
15
|
-
import { ANDROID } from '../../../CommonTestAndPreview/constants';
|
|
16
|
-
import TagList from '../../../../v2Containers/TagList';
|
|
17
|
-
import { isAiContentBotDisabled } from '../../../../utils/common';
|
|
18
|
-
import { SMS } from '../../../../v2Containers/CreativesContainer/constants';
|
|
19
|
-
import { EMBEDDED } from '../../../../constants/unified';
|
|
20
|
-
import { createFieldRegistry } from '../core/schema/fieldRegistry';
|
|
21
|
-
import { FIELD_TYPE } from '../constants';
|
|
22
|
-
import { fieldIds, SMS_TAB } from '../channels/sms/config';
|
|
23
|
-
|
|
24
|
-
const { TextArea } = CapInput;
|
|
25
|
-
|
|
26
|
-
// Shared prop shapes — every renderer receives the frozen field schema and the
|
|
27
|
-
// render context; data renderers also get value/error + an onChange handler.
|
|
28
|
-
const dataFieldPropTypes = {
|
|
29
|
-
field: PropTypes.object.isRequired,
|
|
30
|
-
value: PropTypes.any,
|
|
31
|
-
error: PropTypes.any,
|
|
32
|
-
onChange: PropTypes.func.isRequired,
|
|
33
|
-
renderContext: PropTypes.object.isRequired,
|
|
34
|
-
};
|
|
35
|
-
const dataFieldDefaultProps = {
|
|
36
|
-
value: undefined,
|
|
37
|
-
error: undefined,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const InputField = ({
|
|
41
|
-
field, value, error, onChange, onBlur, renderContext,
|
|
42
|
-
}) => {
|
|
43
|
-
// Errors show only after validation is triggered (checkValidation). We drive the
|
|
44
|
-
// border via `status` and render the message in our own span rather than passing
|
|
45
|
-
// CapInput's errorMessage: toggling its antd suffix remounts the <input> and drops
|
|
46
|
-
// focus mid-keystroke.
|
|
47
|
-
const showError = Boolean(renderContext?.checkValidation && error);
|
|
48
|
-
return (
|
|
49
|
-
<CapColumn key={field.id} span={field.width} offset={field.offset} style={field.style || {}}>
|
|
50
|
-
<CapInput
|
|
51
|
-
id={field.id}
|
|
52
|
-
label={field.label}
|
|
53
|
-
placeholder={field.placeholder}
|
|
54
|
-
status={showError ? 'error' : undefined}
|
|
55
|
-
className={`input-primary chart-name-input${showError ? ' error' : ''}`}
|
|
56
|
-
value={value || ''}
|
|
57
|
-
onChange={(e) => onChange(e.target.value)}
|
|
58
|
-
onBlur={onBlur}
|
|
59
|
-
disabled={field.disabled}
|
|
60
|
-
size={field.size || 'default'}
|
|
61
|
-
/>
|
|
62
|
-
{showError && field.errorMessage ? (
|
|
63
|
-
<span className="component-with-label-error-message">{field.errorMessage}</span>
|
|
64
|
-
) : null}
|
|
65
|
-
</CapColumn>
|
|
66
|
-
);
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
InputField.propTypes = {
|
|
70
|
-
...dataFieldPropTypes,
|
|
71
|
-
onBlur: PropTypes.func,
|
|
72
|
-
};
|
|
73
|
-
InputField.defaultProps = {
|
|
74
|
-
...dataFieldDefaultProps,
|
|
75
|
-
onBlur: undefined,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export const TextAreaField = ({
|
|
79
|
-
field, value, error, resolvedError, onChange, renderContext,
|
|
80
|
-
}) => {
|
|
81
|
-
let aiDisabled = true;
|
|
82
|
-
try { aiDisabled = isAiContentBotDisabled(); } catch (e) { aiDisabled = true; }
|
|
83
|
-
// Match renderTextAreaContent: an EMPTY/required error shows only after
|
|
84
|
-
// validation is triggered; other errors (brace/tag) show live while typing.
|
|
85
|
-
const isContentEmpty = !value || !String(value).trim();
|
|
86
|
-
const showError = Boolean(error) && (isContentEmpty ? Boolean(renderContext?.checkValidation) : true);
|
|
87
|
-
// Inline (below-the-box) message: only the brace error is shown inline, passed in
|
|
88
|
-
// as `resolvedError` by the Shell (full mode only — the single intentional
|
|
89
|
-
// difference from Classic). Missing-tag/empty/generic body errors stay border-only
|
|
90
|
-
// here; the footer carries them.
|
|
91
|
-
const inlineMessage = resolvedError || '';
|
|
92
|
-
return (
|
|
93
|
-
<CapColumn key={field.id} span={field.width} offset={field.offset}>
|
|
94
|
-
<TextArea
|
|
95
|
-
id={field.id}
|
|
96
|
-
label={field.label}
|
|
97
|
-
placeholder={field.placeholder || ''}
|
|
98
|
-
className={`${showError ? 'error-form-builder' : ''}`}
|
|
99
|
-
errorMessage={showError && inlineMessage ? inlineMessage : ''}
|
|
100
|
-
autosize={field.autosize ? field.autosizeParams : false}
|
|
101
|
-
value={value || ''}
|
|
102
|
-
onChange={(e) => onChange(e.target.value)}
|
|
103
|
-
style={field.style || {}}
|
|
104
|
-
disabled={field.disabled}
|
|
105
|
-
/>
|
|
106
|
-
{renderContext?.channel === SMS && !aiDisabled && (
|
|
107
|
-
<CapAskAira.ContentGenerationBot
|
|
108
|
-
text={value || ''}
|
|
109
|
-
setText={(x) => onChange(x)}
|
|
110
|
-
iconPlacement="float-br"
|
|
111
|
-
rootStyle={{ bottom: 'calc(1rem + 0.2rem)', right: '0.2rem' }}
|
|
112
|
-
/>
|
|
113
|
-
)}
|
|
114
|
-
</CapColumn>
|
|
115
|
-
);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
TextAreaField.propTypes = {
|
|
119
|
-
...dataFieldPropTypes,
|
|
120
|
-
resolvedError: PropTypes.string,
|
|
121
|
-
};
|
|
122
|
-
TextAreaField.defaultProps = {
|
|
123
|
-
...dataFieldDefaultProps,
|
|
124
|
-
resolvedError: '',
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
export const CheckboxField = ({
|
|
128
|
-
field, value, error, onChange, renderContext,
|
|
129
|
-
}) => {
|
|
130
|
-
const showError = Boolean(renderContext?.checkValidation && error);
|
|
131
|
-
return (
|
|
132
|
-
<CapColumn key={field.id} span={field.width} offset={field.offset}>
|
|
133
|
-
<CapCheckbox
|
|
134
|
-
key={field.id}
|
|
135
|
-
className={`${showError ? 'error' : ''}`}
|
|
136
|
-
errorMessage={field.errorMessage && showError ? field.errorMessage : ''}
|
|
137
|
-
onChange={(e) => onChange(e.target.checked)}
|
|
138
|
-
checked={Boolean(value)}
|
|
139
|
-
style={field.style || {}}
|
|
140
|
-
disabled={field.disabled}
|
|
141
|
-
inductiveText={field.inductiveText}
|
|
142
|
-
>
|
|
143
|
-
{field.label}
|
|
144
|
-
</CapCheckbox>
|
|
145
|
-
</CapColumn>
|
|
146
|
-
);
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
CheckboxField.propTypes = { ...dataFieldPropTypes };
|
|
150
|
-
CheckboxField.defaultProps = { ...dataFieldDefaultProps };
|
|
151
|
-
|
|
152
|
-
export const ButtonField = ({ field, onEvent }) => {
|
|
153
|
-
if (field.metaType !== 'submit-button') return null;
|
|
154
|
-
return (
|
|
155
|
-
<CapColumn style={field.colStyle || {}} span={field.width} offset={field.offset}>
|
|
156
|
-
<CapButton
|
|
157
|
-
type={field.buttonType}
|
|
158
|
-
icon={field.icon}
|
|
159
|
-
disabled={field.disabled || false}
|
|
160
|
-
onClick={(data) => onEvent(field.submitAction, data)}
|
|
161
|
-
>
|
|
162
|
-
{field.value}
|
|
163
|
-
</CapButton>
|
|
164
|
-
</CapColumn>
|
|
165
|
-
);
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
ButtonField.propTypes = {
|
|
169
|
-
field: PropTypes.object.isRequired,
|
|
170
|
-
onEvent: PropTypes.func.isRequired,
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
export const TagListField = ({ field, onEvent, renderContext }) => (
|
|
174
|
-
<CapColumn key={`input-${field.id}`} offset={field.offset} span={field.width || ''} style={field.style || { marginBottom: '16px' }}>
|
|
175
|
-
<TagList
|
|
176
|
-
key={`input-${field.id}`}
|
|
177
|
-
moduleFilterEnabled={renderContext?.location?.query?.type !== EMBEDDED}
|
|
178
|
-
label={field.label || ''}
|
|
179
|
-
onTagSelect={(data) => onEvent('onTagSelect', data)}
|
|
180
|
-
onContextChange={renderContext?.onContextChange}
|
|
181
|
-
location={renderContext?.location}
|
|
182
|
-
tags={renderContext?.tags || []}
|
|
183
|
-
injectedTags={renderContext?.injectedTags || {}}
|
|
184
|
-
className={field.className || ''}
|
|
185
|
-
id={field.id}
|
|
186
|
-
userLocale={renderContext?.userLocale}
|
|
187
|
-
selectedOfferDetails={renderContext?.selectedOfferDetails}
|
|
188
|
-
channel={renderContext?.channel}
|
|
189
|
-
eventContextTags={renderContext?.eventContextTags}
|
|
190
|
-
restrictPersonalization={renderContext?.restrictPersonalization}
|
|
191
|
-
waitEventContextTags={renderContext?.waitEventContextTags}
|
|
192
|
-
/>
|
|
193
|
-
</CapColumn>
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
TagListField.propTypes = {
|
|
197
|
-
field: PropTypes.object.isRequired,
|
|
198
|
-
onEvent: PropTypes.func.isRequired,
|
|
199
|
-
renderContext: PropTypes.object.isRequired,
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
export const SmsPreviewField = ({ field, renderContext }) => {
|
|
203
|
-
const tab = renderContext?.legacyFormData?.[SMS_TAB] || {};
|
|
204
|
-
const content = tab[fieldIds.editor];
|
|
205
|
-
const unicodeEnabled = tab[fieldIds.unicode];
|
|
206
|
-
return (
|
|
207
|
-
<CapColumn key="input" span={23} offset={1}>
|
|
208
|
-
<UnifiedPreview
|
|
209
|
-
key={field.id}
|
|
210
|
-
style={field.customStyling || {}}
|
|
211
|
-
channel={field.channel || SMS}
|
|
212
|
-
content={content}
|
|
213
|
-
device={ANDROID}
|
|
214
|
-
showDeviceToggle={false}
|
|
215
|
-
showHeader={false}
|
|
216
|
-
formatMessage={renderContext?.intl?.formatMessage}
|
|
217
|
-
senderId={unicodeEnabled ? 'Unicode' : 'ASCII'}
|
|
218
|
-
/>
|
|
219
|
-
</CapColumn>
|
|
220
|
-
);
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
SmsPreviewField.propTypes = {
|
|
224
|
-
field: PropTypes.object.isRequired,
|
|
225
|
-
renderContext: PropTypes.object.isRequired,
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
// sms-count: a display-only div whose text comes from the field value/derived state.
|
|
229
|
-
// Matches the class component's renderPrimitiveElement: the field `style`
|
|
230
|
-
// (e.g. float:right) goes on the inner <div>; the CapColumn gets `colStyle`
|
|
231
|
-
// (not `style`) — applying the float to the column shifts the layout.
|
|
232
|
-
export const DivField = ({ field, renderContext }) => {
|
|
233
|
-
const tab = renderContext?.legacyFormData?.[SMS_TAB] || {};
|
|
234
|
-
const children = tab[field.id] || field.value || '';
|
|
235
|
-
return (
|
|
236
|
-
<CapColumn key={field.id} span={field.width} offset={field.offset} style={field.colStyle || {}}>
|
|
237
|
-
<div
|
|
238
|
-
id={field.id}
|
|
239
|
-
className={field.className || ''}
|
|
240
|
-
ref={renderContext?.refs?.[field.id]}
|
|
241
|
-
style={field.style || {}}
|
|
242
|
-
>
|
|
243
|
-
{children}
|
|
244
|
-
</div>
|
|
245
|
-
</CapColumn>
|
|
246
|
-
);
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
DivField.propTypes = {
|
|
250
|
-
field: PropTypes.object.isRequired,
|
|
251
|
-
renderContext: PropTypes.object.isRequired,
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
/** Build a registry pre-loaded with the SMS field renderers. */
|
|
255
|
-
export const createSmsRegistry = () => createFieldRegistry({
|
|
256
|
-
[FIELD_TYPE.INPUT]: InputField,
|
|
257
|
-
[FIELD_TYPE.TEXTAREA]: TextAreaField,
|
|
258
|
-
[FIELD_TYPE.CHECKBOX]: CheckboxField,
|
|
259
|
-
[FIELD_TYPE.BUTTON]: ButtonField,
|
|
260
|
-
[FIELD_TYPE.TAG_LIST]: TagListField,
|
|
261
|
-
[FIELD_TYPE.SMS_PREVIEW]: SmsPreviewField,
|
|
262
|
-
[FIELD_TYPE.DIV]: DivField,
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
export default createSmsRegistry;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the channel adapter registry lookup.
|
|
3
|
-
*/
|
|
4
|
-
import { getChannelAdapter } from '../channels/registry';
|
|
5
|
-
import smsAdapter from '../channels/sms';
|
|
6
|
-
|
|
7
|
-
describe('getChannelAdapter', () => {
|
|
8
|
-
it('resolves the SMS adapter (case-insensitive)', () => {
|
|
9
|
-
expect(getChannelAdapter('SMS')).toBe(smsAdapter);
|
|
10
|
-
expect(getChannelAdapter('sms')).toBe(smsAdapter);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('returns null for an unknown channel', () => {
|
|
14
|
-
expect(getChannelAdapter('EMAIL')).toBeNull();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('returns null for undefined/empty channel', () => {
|
|
18
|
-
expect(getChannelAdapter(undefined)).toBeNull();
|
|
19
|
-
expect(getChannelAdapter('')).toBeNull();
|
|
20
|
-
});
|
|
21
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the field renderer registry (factory).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { createFieldRegistry } from '../core/schema/fieldRegistry';
|
|
6
|
-
|
|
7
|
-
const Input = () => null;
|
|
8
|
-
const TextArea = () => null;
|
|
9
|
-
const EmailCk = () => null;
|
|
10
|
-
|
|
11
|
-
describe('createFieldRegistry', () => {
|
|
12
|
-
it('registers and resolves a renderer by type', () => {
|
|
13
|
-
const r = createFieldRegistry();
|
|
14
|
-
r.register('input', Input);
|
|
15
|
-
expect(r.resolve('input')).toBe(Input);
|
|
16
|
-
expect(r.has('input')).toBe(true);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('returns null for an unregistered type', () => {
|
|
20
|
-
const r = createFieldRegistry();
|
|
21
|
-
expect(r.resolve('nope')).toBeNull();
|
|
22
|
-
expect(r.has('nope')).toBe(false);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('registerAll adds many at once and is chainable', () => {
|
|
26
|
-
const r = createFieldRegistry();
|
|
27
|
-
const ret = r.registerAll({ input: Input, textarea: TextArea });
|
|
28
|
-
expect(ret).toBe(r);
|
|
29
|
-
expect(r.resolve('input')).toBe(Input);
|
|
30
|
-
expect(r.resolve('textarea')).toBe(TextArea);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('registerAll() with no args is a chainable no-op (default {})', () => {
|
|
34
|
-
const r = createFieldRegistry();
|
|
35
|
-
expect(r.registerAll()).toBe(r);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('register() is chainable', () => {
|
|
39
|
-
const r = createFieldRegistry();
|
|
40
|
-
expect(r.register('input', Input)).toBe(r);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('accepts an initial map in the factory', () => {
|
|
44
|
-
const r = createFieldRegistry({ input: Input });
|
|
45
|
-
expect(r.resolve('input')).toBe(Input);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('extend() shadows the base registry without mutating it (channel overrides)', () => {
|
|
49
|
-
const base = createFieldRegistry({ input: Input, ckeditor: TextArea });
|
|
50
|
-
const emailScoped = base.extend({ ckeditor: EmailCk });
|
|
51
|
-
|
|
52
|
-
// override applies only to the extended registry
|
|
53
|
-
expect(emailScoped.resolve('ckeditor')).toBe(EmailCk);
|
|
54
|
-
expect(emailScoped.resolve('input')).toBe(Input); // inherited
|
|
55
|
-
// base is untouched
|
|
56
|
-
expect(base.resolve('ckeditor')).toBe(TextArea);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('register on the child does not leak into the parent', () => {
|
|
60
|
-
const base = createFieldRegistry({ input: Input });
|
|
61
|
-
const child = base.extend();
|
|
62
|
-
child.register('textarea', TextArea);
|
|
63
|
-
expect(base.has('textarea')).toBe(false);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FieldSlot memo-boundary tests. Proves the perf contract: a field re-renders
|
|
3
|
-
* only when its own primitive props (value/error/checkValidation) change, not
|
|
4
|
-
* when an unrelated re-render happens or a sibling field changes.
|
|
5
|
-
*/
|
|
6
|
-
import React from 'react';
|
|
7
|
-
import { render } from '@testing-library/react';
|
|
8
|
-
import FieldSlot from '../layout/FieldSlot';
|
|
9
|
-
|
|
10
|
-
const field = { id: 'template-name', type: 'input', width: 18 };
|
|
11
|
-
const noop = () => {};
|
|
12
|
-
|
|
13
|
-
const renderSlot = (overrides = {}) => {
|
|
14
|
-
const Renderer = jest.fn(() => <div>field</div>);
|
|
15
|
-
const props = {
|
|
16
|
-
Renderer,
|
|
17
|
-
field,
|
|
18
|
-
value: 'a',
|
|
19
|
-
error: false,
|
|
20
|
-
checkValidation: false,
|
|
21
|
-
channel: 'SMS',
|
|
22
|
-
onFieldChange: noop,
|
|
23
|
-
onFieldBlur: noop,
|
|
24
|
-
onEvent: noop,
|
|
25
|
-
...overrides,
|
|
26
|
-
};
|
|
27
|
-
const utils = render(<FieldSlot {...props} />);
|
|
28
|
-
return { Renderer, props, rerender: (next) => utils.rerender(<FieldSlot {...{ ...props, ...next }} />) };
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
describe('FieldSlot memoization', () => {
|
|
32
|
-
it('does NOT re-render the field when props are unchanged (stable handlers)', () => {
|
|
33
|
-
const { Renderer, rerender } = renderSlot();
|
|
34
|
-
expect(Renderer).toHaveBeenCalledTimes(1);
|
|
35
|
-
rerender({}); // identical props -> memo bails out
|
|
36
|
-
expect(Renderer).toHaveBeenCalledTimes(1);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('re-renders when its own value changes', () => {
|
|
40
|
-
const { Renderer, rerender } = renderSlot();
|
|
41
|
-
rerender({ value: 'ab' });
|
|
42
|
-
expect(Renderer).toHaveBeenCalledTimes(2);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('re-renders when its error changes', () => {
|
|
46
|
-
const { Renderer, rerender } = renderSlot();
|
|
47
|
-
rerender({ error: 'tagBracketCountMismatchError' });
|
|
48
|
-
expect(Renderer).toHaveBeenCalledTimes(2);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('re-renders when checkValidation flips (error reveal on Save)', () => {
|
|
52
|
-
const { Renderer, rerender } = renderSlot();
|
|
53
|
-
rerender({ checkValidation: true });
|
|
54
|
-
expect(Renderer).toHaveBeenCalledTimes(2);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('passes a minimal renderContext ({ checkValidation, channel }) and wires onChange to its field', () => {
|
|
58
|
-
const onFieldChange = jest.fn();
|
|
59
|
-
let captured;
|
|
60
|
-
const Renderer = jest.fn((p) => { captured = p; return <div>f</div>; });
|
|
61
|
-
render(
|
|
62
|
-
<FieldSlot
|
|
63
|
-
Renderer={Renderer}
|
|
64
|
-
field={field}
|
|
65
|
-
value="a"
|
|
66
|
-
error={false}
|
|
67
|
-
checkValidation
|
|
68
|
-
channel="SMS"
|
|
69
|
-
onFieldChange={onFieldChange}
|
|
70
|
-
onFieldBlur={noop}
|
|
71
|
-
onEvent={noop}
|
|
72
|
-
/>,
|
|
73
|
-
);
|
|
74
|
-
expect(captured.renderContext).toEqual({ checkValidation: true, channel: 'SMS' });
|
|
75
|
-
captured.onChange('hello');
|
|
76
|
-
expect(onFieldChange).toHaveBeenCalledWith(field, 'hello');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('wires onBlur and onEvent to the field handlers', () => {
|
|
80
|
-
const onFieldBlur = jest.fn();
|
|
81
|
-
const onEvent = jest.fn();
|
|
82
|
-
let captured;
|
|
83
|
-
const Renderer = jest.fn((p) => { captured = p; return <div>f</div>; });
|
|
84
|
-
render(<FieldSlot Renderer={Renderer} field={field} onFieldChange={noop} onFieldBlur={onFieldBlur} onEvent={onEvent} />);
|
|
85
|
-
captured.onBlur();
|
|
86
|
-
captured.onEvent('onTagSelect', { x: 1 });
|
|
87
|
-
expect(onFieldBlur).toHaveBeenCalledWith(field);
|
|
88
|
-
expect(onEvent).toHaveBeenCalledWith(field, 'onTagSelect', { x: 1 });
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('onBlur / onEvent are safe no-ops when those handlers are omitted (optional-chaining branch)', () => {
|
|
92
|
-
let captured;
|
|
93
|
-
const Renderer = jest.fn((p) => { captured = p; return <div>f</div>; });
|
|
94
|
-
render(<FieldSlot Renderer={Renderer} field={field} onFieldChange={noop} />);
|
|
95
|
-
expect(() => { captured.onBlur(); captured.onEvent('e', {}); }).not.toThrow();
|
|
96
|
-
});
|
|
97
|
-
});
|