@capillarytech/creatives-library 9.0.29-alpha.1 → 9.0.29-alpha.3
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/package.json +1 -1
- package/utils/rcsPayloadUtils.js +2 -5
- package/utils/tests/rcsPayloadUtils.test.js +2 -54
- package/v2Components/CapActionButton/constants.js +0 -1
- package/v2Components/CapActionButton/index.js +9 -76
- package/v2Components/CapActionButton/index.scss +0 -13
- package/v2Components/CapActionButton/messages.js +0 -13
- package/v2Components/CapActionButton/tests/index.test.js +1 -32
- package/v2Containers/Rcs/carouselUtils.js +79 -53
- package/v2Containers/Rcs/components/CarouselCard.js +9 -11
- package/v2Containers/Rcs/components/CarouselCardButtons.js +0 -20
- package/v2Containers/Rcs/constants.js +7 -2
- package/v2Containers/Rcs/index.js +151 -56
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +43 -5
- package/v2Containers/Rcs/tests/CarouselCard.test.js +2 -1
- package/v2Containers/Rcs/tests/carouselUtils.test.js +15 -15
- package/v2Containers/Rcs/tests/index.test.js +265 -84
- package/v2Containers/Rcs/tests/utils.test.js +80 -0
- package/v2Containers/Rcs/utils.js +63 -6
package/package.json
CHANGED
package/utils/rcsPayloadUtils.js
CHANGED
|
@@ -59,12 +59,9 @@ export const normalizeRcsMessageContentForApi = messageContentItem => {
|
|
|
59
59
|
rcsCardPayloadOnly.cardContent.forEach(card => {
|
|
60
60
|
if (Array.isArray(card?.suggestions)) {
|
|
61
61
|
card.suggestions.forEach(suggestion => {
|
|
62
|
-
// isSaved
|
|
63
|
-
//
|
|
64
|
-
// (if any) already lives inline in `url` (e.g. "https://x.com{{first_name}}") — no
|
|
65
|
-
// separate dynamicUrlPayload field is sent for RCS.
|
|
62
|
+
// isSaved is UI-only editor state: campaigns' send-for-approval API rejects it, and it
|
|
63
|
+
// isn't part of the persisted payload shape.
|
|
66
64
|
delete suggestion?.isSaved;
|
|
67
|
-
delete suggestion?.urlType;
|
|
68
65
|
});
|
|
69
66
|
}
|
|
70
67
|
});
|
|
@@ -209,7 +209,7 @@ describe('normalizeRcsMessageContentForApi', () => {
|
|
|
209
209
|
expect(item.rcsContent.cardContent).toEqual([{ title: 'Card' }]);
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
-
it('strips isSaved
|
|
212
|
+
it('strips isSaved from each suggestion in cardContent', () => {
|
|
213
213
|
const item = {
|
|
214
214
|
rcsContent: {
|
|
215
215
|
cardContent: [
|
|
@@ -217,9 +217,7 @@ describe('normalizeRcsMessageContentForApi', () => {
|
|
|
217
217
|
title: 'Card 1',
|
|
218
218
|
suggestions: [
|
|
219
219
|
{ type: 'QUICK_REPLY', text: 'Yes', isSaved: true },
|
|
220
|
-
{
|
|
221
|
-
type: 'CTA', text: 'Go', url: 'https://example.com', urlType: 'STATIC', isSaved: false,
|
|
222
|
-
},
|
|
220
|
+
{ type: 'CTA', text: 'Go', url: 'https://example.com', isSaved: false },
|
|
223
221
|
],
|
|
224
222
|
},
|
|
225
223
|
],
|
|
@@ -232,56 +230,6 @@ describe('normalizeRcsMessageContentForApi', () => {
|
|
|
232
230
|
]);
|
|
233
231
|
});
|
|
234
232
|
|
|
235
|
-
it('keeps type "CTA" and the inline tag in url, but drops urlType, for a dynamic-URL suggestion', () => {
|
|
236
|
-
const item = {
|
|
237
|
-
rcsContent: {
|
|
238
|
-
cardContent: [
|
|
239
|
-
{
|
|
240
|
-
title: 'Card 1',
|
|
241
|
-
suggestions: [
|
|
242
|
-
{
|
|
243
|
-
type: 'CTA',
|
|
244
|
-
text: 'Visit here',
|
|
245
|
-
url: 'https://example.com/{{first_name}}',
|
|
246
|
-
urlType: 'DYNAMIC',
|
|
247
|
-
isSaved: true,
|
|
248
|
-
},
|
|
249
|
-
],
|
|
250
|
-
},
|
|
251
|
-
],
|
|
252
|
-
},
|
|
253
|
-
};
|
|
254
|
-
normalizeRcsMessageContentForApi(item);
|
|
255
|
-
expect(item.rcsContent.cardContent[0].suggestions).toEqual([
|
|
256
|
-
{
|
|
257
|
-
type: 'CTA',
|
|
258
|
-
text: 'Visit here',
|
|
259
|
-
url: 'https://example.com/{{first_name}}',
|
|
260
|
-
},
|
|
261
|
-
]);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('leaves phone number and quick reply suggestions untouched by the urlType stripping', () => {
|
|
265
|
-
const item = {
|
|
266
|
-
rcsContent: {
|
|
267
|
-
cardContent: [
|
|
268
|
-
{
|
|
269
|
-
title: 'Card 1',
|
|
270
|
-
suggestions: [
|
|
271
|
-
{
|
|
272
|
-
type: 'PHONE_NUMBER', text: 'Call', phoneNumber: '918987678', isSaved: true,
|
|
273
|
-
},
|
|
274
|
-
],
|
|
275
|
-
},
|
|
276
|
-
],
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
normalizeRcsMessageContentForApi(item);
|
|
280
|
-
expect(item.rcsContent.cardContent[0].suggestions).toEqual([
|
|
281
|
-
{ type: 'PHONE_NUMBER', text: 'Call', phoneNumber: '918987678' },
|
|
282
|
-
]);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
233
|
it('handles cardContent with no suggestions gracefully', () => {
|
|
286
234
|
const item = {
|
|
287
235
|
rcsContent: {
|
|
@@ -43,7 +43,6 @@ export const RCS_CTA_URL_TYPE = {
|
|
|
43
43
|
export const BTN_MAX_LENGTH = 20;
|
|
44
44
|
export const PHONE_NUMBER_MAX_LENGTH = 15;
|
|
45
45
|
export const URL_MAX_LENGTH = 2000;
|
|
46
|
-
export const DYNAMIC_URL_SUFFIX = '{{1}}';
|
|
47
46
|
|
|
48
47
|
export const CTA_TYPE_RADIO_OPTIONS = [
|
|
49
48
|
{ label: <FormattedMessage {...messages.ctaPhoneNo} />, value: RCS_BUTTON_TYPES.PHONE_NUMBER },
|
|
@@ -17,8 +17,7 @@ import CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
|
17
17
|
import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
|
|
18
18
|
import CapTooltipWithInfo from '@capillarytech/cap-ui-library/CapTooltipWithInfo';
|
|
19
19
|
import { CAP_SPACE_04 } from '@capillarytech/cap-ui-library/styled/variables';
|
|
20
|
-
import globalMessages from '../../v2Containers/Cap/messages';
|
|
21
|
-
import TagList from '../../v2Containers/TagList';
|
|
20
|
+
import globalMessages from '../../v2Containers/Cap/messages';import CapTagListWithInput from '../CapTagListWithInput';
|
|
22
21
|
|
|
23
22
|
import { isUrl, isValidText } from '../../v2Containers/Line/Container/Wrapper/utils';
|
|
24
23
|
import messages from './messages';
|
|
@@ -31,7 +30,6 @@ import {
|
|
|
31
30
|
RCS_CTA_URL_TYPE,
|
|
32
31
|
CTA_TYPE_RADIO_OPTIONS,
|
|
33
32
|
CTA_URL_TYPE_SELECT_OPTIONS,
|
|
34
|
-
DYNAMIC_URL_SUFFIX,
|
|
35
33
|
} from './constants';
|
|
36
34
|
import './index.scss';
|
|
37
35
|
import { INITIAL_SUGGESTIONS, RCS_BUTTON_TYPES, HOST_ICS} from '../../v2Containers/Rcs/constants';
|
|
@@ -58,7 +56,6 @@ export const CapActionButton = (props) => {
|
|
|
58
56
|
minSavedSuggestions = 0,
|
|
59
57
|
hideDeleteSuggestionIndexes = [],
|
|
60
58
|
} = props;
|
|
61
|
-
const { formatMessage } = intl;
|
|
62
59
|
const isHostIcs = host === HOST_ICS;
|
|
63
60
|
const [urlError, setUrlError] = useState(false);
|
|
64
61
|
const [buttonError, setButtonError] = useState(false);
|
|
@@ -125,8 +122,7 @@ export const CapActionButton = (props) => {
|
|
|
125
122
|
return formatMessage(messages.ctaWebsiteUrlErrorMessage);
|
|
126
123
|
}
|
|
127
124
|
if (urlSubtype === RCS_CTA_URL_TYPE.DYNAMIC) {
|
|
128
|
-
|
|
129
|
-
return isUrl(baseUrl) ? false : formatMessage(messages.ctaWebsiteUrlErrorMessage);
|
|
125
|
+
return false;
|
|
130
126
|
}
|
|
131
127
|
if (!isUrl(trimmedUrl)) {
|
|
132
128
|
return formatMessage(messages.ctaWebsiteUrlErrorMessage);
|
|
@@ -138,12 +134,9 @@ export const CapActionButton = (props) => {
|
|
|
138
134
|
};
|
|
139
135
|
|
|
140
136
|
const onUrlChange = ({ target }) => {
|
|
141
|
-
const { id } = target;
|
|
137
|
+
const { value, id } = target;
|
|
142
138
|
const row = suggestions[id] || {};
|
|
143
139
|
const subtype = row.urlType || RCS_CTA_URL_TYPE.STATIC;
|
|
144
|
-
const value = subtype === RCS_CTA_URL_TYPE.DYNAMIC
|
|
145
|
-
? target.value.replace(invalidVarRegex, '')
|
|
146
|
-
: target.value;
|
|
147
140
|
setUrlError(validateCtaUrlValue(value, subtype));
|
|
148
141
|
updateHandler(HANDLERS.URL, value, id);
|
|
149
142
|
};
|
|
@@ -156,14 +149,6 @@ export const CapActionButton = (props) => {
|
|
|
156
149
|
updateButtonChange(cloned, index);
|
|
157
150
|
};
|
|
158
151
|
|
|
159
|
-
const onTagSelect = (data, index, url) => {
|
|
160
|
-
updateHandler(HANDLERS.URL, (url || '').replace(DYNAMIC_URL_SUFFIX, `{{${data}}}`), index);
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const revertTagSelect = (index, url) => {
|
|
164
|
-
updateHandler(HANDLERS.URL, (url || '').replace(invalidVarRegex, DYNAMIC_URL_SUFFIX), index);
|
|
165
|
-
};
|
|
166
|
-
|
|
167
152
|
const onPhoneNoChange = (value, index) => {
|
|
168
153
|
updateHandler(HANDLERS.PHONE_NUMBER, value, index);
|
|
169
154
|
};
|
|
@@ -176,25 +161,12 @@ export const CapActionButton = (props) => {
|
|
|
176
161
|
return true;
|
|
177
162
|
} if (type === RCS_BUTTON_TYPES.CTA) {
|
|
178
163
|
const subtype = urlType || RCS_CTA_URL_TYPE.STATIC;
|
|
179
|
-
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
182
|
-
// A dynamic URL still holding the unresolved {{1}} placeholder has no real
|
|
183
|
-
// personalization tag assigned yet — block save until one is picked via "Add URL label".
|
|
184
|
-
return subtype === RCS_CTA_URL_TYPE.DYNAMIC && (url || '').includes(DYNAMIC_URL_SUFFIX);
|
|
164
|
+
return !!validateCtaUrlValue(url, subtype);
|
|
185
165
|
}
|
|
186
166
|
return false;
|
|
187
167
|
};
|
|
188
168
|
|
|
189
169
|
const saveCta = (index) => {
|
|
190
|
-
const cta = suggestions[index];
|
|
191
|
-
if (
|
|
192
|
-
cta?.type === RCS_BUTTON_TYPES.CTA
|
|
193
|
-
&& cta?.urlType === RCS_CTA_URL_TYPE.DYNAMIC
|
|
194
|
-
&& !(cta?.url || '').includes(DYNAMIC_URL_SUFFIX)
|
|
195
|
-
) {
|
|
196
|
-
cta.url = `${cta.url || ''}${DYNAMIC_URL_SUFFIX}`;
|
|
197
|
-
}
|
|
198
170
|
updateHandler(HANDLERS.IS_SAVED, true, index);
|
|
199
171
|
};
|
|
200
172
|
|
|
@@ -208,6 +180,8 @@ export const CapActionButton = (props) => {
|
|
|
208
180
|
updateButtonChange(newSuggestion, suggestions?.length);
|
|
209
181
|
};
|
|
210
182
|
|
|
183
|
+
const { formatMessage } = intl;
|
|
184
|
+
|
|
211
185
|
const hideDeleteForSuggestionIndex = (idx) =>
|
|
212
186
|
Array.isArray(hideDeleteSuggestionIndexes) && hideDeleteSuggestionIndexes.includes(idx);
|
|
213
187
|
|
|
@@ -341,32 +315,13 @@ export const CapActionButton = (props) => {
|
|
|
341
315
|
</CapColumn>
|
|
342
316
|
<CapColumn span={18} className="rcs-cta-url-value-col">
|
|
343
317
|
<CapHeading type="h4" className="cta-label">
|
|
344
|
-
{formatMessage(messages.
|
|
345
|
-
{urlSubtype === RCS_CTA_URL_TYPE.DYNAMIC && (
|
|
346
|
-
<CapTooltipWithInfo
|
|
347
|
-
infoIconProps={{
|
|
348
|
-
style: { marginLeft: CAP_SPACE_04 },
|
|
349
|
-
}}
|
|
350
|
-
autoAdjustOverflow
|
|
351
|
-
placement="right"
|
|
352
|
-
title={formatMessage(messages.ctaDynamicUrlTooltip, { one: DYNAMIC_URL_SUFFIX })}
|
|
353
|
-
/>
|
|
354
|
-
)}
|
|
318
|
+
{formatMessage(messages.ctaUrlField)}
|
|
355
319
|
</CapHeading>
|
|
356
320
|
<CapInput
|
|
357
321
|
id={index}
|
|
358
|
-
addonAfter={
|
|
359
|
-
urlSubtype === RCS_CTA_URL_TYPE.DYNAMIC
|
|
360
|
-
&& !(url || '').includes(DYNAMIC_URL_SUFFIX)
|
|
361
|
-
&& DYNAMIC_URL_SUFFIX
|
|
362
|
-
}
|
|
363
322
|
className="rcs-cta-url"
|
|
364
323
|
onChange={onUrlChange}
|
|
365
|
-
placeholder={
|
|
366
|
-
urlSubtype === RCS_CTA_URL_TYPE.DYNAMIC
|
|
367
|
-
? formatMessage(messages.ctaDynamicPlaceholder)
|
|
368
|
-
: formatMessage(messages.ctaEnterUrlPlaceholder)
|
|
369
|
-
}
|
|
324
|
+
placeholder={formatMessage(messages.ctaEnterUrlPlaceholder)}
|
|
370
325
|
value={url || ''}
|
|
371
326
|
size="large"
|
|
372
327
|
maxLength={URL_MAX_LENGTH}
|
|
@@ -448,7 +403,7 @@ export const CapActionButton = (props) => {
|
|
|
448
403
|
span={1}
|
|
449
404
|
className={`${ctaIsPhone ? 'whatsapp-saved-cta-phone-icon' : ''}`}
|
|
450
405
|
>
|
|
451
|
-
<CapIcon size="s" type={ctaIsPhone ? 'call' : (ctaIsReply ? 'small-link' : '
|
|
406
|
+
<CapIcon size="s" type={ctaIsPhone ? 'call' : (ctaIsReply ? 'small-link' : 'launch')} />
|
|
452
407
|
</CapColumn>
|
|
453
408
|
<CapColumn span={6}>
|
|
454
409
|
<CapLabel
|
|
@@ -475,28 +430,6 @@ export const CapActionButton = (props) => {
|
|
|
475
430
|
</CapTooltip>
|
|
476
431
|
</>
|
|
477
432
|
)}
|
|
478
|
-
{isEditFlow && urlSubtype === RCS_CTA_URL_TYPE.DYNAMIC && (url || '').includes(DYNAMIC_URL_SUFFIX) && (
|
|
479
|
-
<TagList
|
|
480
|
-
className="rcs-cta-taglist"
|
|
481
|
-
label={formatMessage(messages.ctaTagListLabel)}
|
|
482
|
-
onTagSelect={(data) => onTagSelect(data, index, url)}
|
|
483
|
-
location={location}
|
|
484
|
-
tags={tags}
|
|
485
|
-
injectedTags={injectedTags}
|
|
486
|
-
selectedOfferDetails={selectedOfferDetails}
|
|
487
|
-
onContextChange={onContextChange}
|
|
488
|
-
/>
|
|
489
|
-
)}
|
|
490
|
-
{isEditFlow && urlSubtype === RCS_CTA_URL_TYPE.DYNAMIC && !(url || '').includes(DYNAMIC_URL_SUFFIX) && (
|
|
491
|
-
<CapTooltip title={formatMessage(messages.ctaTagListRevert)} placement="top">
|
|
492
|
-
<CapIcon
|
|
493
|
-
size="s"
|
|
494
|
-
type="return"
|
|
495
|
-
className="rcs-cta-tag-revert"
|
|
496
|
-
onClick={() => revertTagSelect(index, url)}
|
|
497
|
-
/>
|
|
498
|
-
</CapTooltip>
|
|
499
|
-
)}
|
|
500
433
|
{(isFullMode && !isEditFlow) && (
|
|
501
434
|
<div className="rcs-saved-cta-action-icons">
|
|
502
435
|
<CapIcon
|
|
@@ -84,19 +84,6 @@
|
|
|
84
84
|
.rcs-saved-cta-delete-icon {
|
|
85
85
|
cursor: pointer;
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
// Dynamic-URL tag controls: keep them from stretching the flex row's
|
|
89
|
-
// height/width — same treatment as the edit/delete icons above.
|
|
90
|
-
.rcs-cta-tag-revert {
|
|
91
|
-
display: flex;
|
|
92
|
-
align-items: center;
|
|
93
|
-
flex-shrink: 0;
|
|
94
|
-
cursor: pointer;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
.rcs-cta-taglist {
|
|
98
|
-
flex-shrink: 0;
|
|
99
|
-
}
|
|
100
87
|
}
|
|
101
88
|
|
|
102
89
|
// Button text / URL: count via CapInput `suffix` (antd affix, right-aligned inside field).
|
|
@@ -199,17 +199,4 @@ export default defineMessages({
|
|
|
199
199
|
id: `${prefix}.addLabels`,
|
|
200
200
|
defaultMessage: 'Add Labels',
|
|
201
201
|
},
|
|
202
|
-
ctaDynamicUrlTooltip: {
|
|
203
|
-
id: `${prefix}.ctaDynamicUrlTooltip`,
|
|
204
|
-
defaultMessage:
|
|
205
|
-
'Only one variable can be added to a URL. No need to add {one} to the end of the URL',
|
|
206
|
-
},
|
|
207
|
-
ctaTagListLabel: {
|
|
208
|
-
id: `${prefix}.ctaTagListLabel`,
|
|
209
|
-
defaultMessage: 'Add URL label',
|
|
210
|
-
},
|
|
211
|
-
ctaTagListRevert: {
|
|
212
|
-
id: `${prefix}.ctaTagListRevert`,
|
|
213
|
-
defaultMessage: 'Reset website URL label to default value',
|
|
214
|
-
},
|
|
215
202
|
});
|
|
@@ -4,9 +4,7 @@ import '@testing-library/jest-dom';
|
|
|
4
4
|
import { render, screen, fireEvent } from '../../../utils/test-utils';
|
|
5
5
|
import { waitFor } from '@testing-library/react';
|
|
6
6
|
import { CapActionButton } from '../index';
|
|
7
|
-
import {
|
|
8
|
-
BTN_MAX_LENGTH, PHONE_NUMBER_MAX_LENGTH, URL_MAX_LENGTH, RCS_CTA_URL_TYPE, DYNAMIC_URL_SUFFIX,
|
|
9
|
-
} from '../constants';
|
|
7
|
+
import { BTN_MAX_LENGTH, PHONE_NUMBER_MAX_LENGTH, URL_MAX_LENGTH } from '../constants';
|
|
10
8
|
import { RCS_BUTTON_TYPES, HOST_ICS } from '../../../v2Containers/Rcs/constants';
|
|
11
9
|
|
|
12
10
|
const updateHandler = jest.fn();
|
|
@@ -29,7 +27,6 @@ const initializeComponent = (
|
|
|
29
27
|
text: d.displayText ?? d.text ?? '',
|
|
30
28
|
phoneNumber: d.phoneNumber ?? '',
|
|
31
29
|
url: d.url ?? '',
|
|
32
|
-
urlType: d.urlType,
|
|
33
30
|
postback: d.postback ?? '',
|
|
34
31
|
isSaved: d.isSaved ?? false,
|
|
35
32
|
}));
|
|
@@ -554,34 +551,6 @@ describe('CapActionButton', () => {
|
|
|
554
551
|
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
|
|
555
552
|
});
|
|
556
553
|
|
|
557
|
-
it('should disable save for a dynamic-URL CTA still holding the unresolved {{1}} placeholder', () => {
|
|
558
|
-
const initial = {
|
|
559
|
-
index: 0,
|
|
560
|
-
type: RCS_BUTTON_TYPES.CTA,
|
|
561
|
-
text: 'Visit here',
|
|
562
|
-
url: `https://example.com${DYNAMIC_URL_SUFFIX}`,
|
|
563
|
-
urlType: RCS_CTA_URL_TYPE.DYNAMIC,
|
|
564
|
-
postback: 'Visit here',
|
|
565
|
-
isSaved: false,
|
|
566
|
-
};
|
|
567
|
-
initializeComponent([initial]);
|
|
568
|
-
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
it('should enable save for a dynamic-URL CTA once the placeholder has been replaced with a real tag', () => {
|
|
572
|
-
const initial = {
|
|
573
|
-
index: 0,
|
|
574
|
-
type: RCS_BUTTON_TYPES.CTA,
|
|
575
|
-
text: 'Visit here',
|
|
576
|
-
url: 'https://example.com{{first_name}}',
|
|
577
|
-
urlType: RCS_CTA_URL_TYPE.DYNAMIC,
|
|
578
|
-
postback: 'Visit here',
|
|
579
|
-
isSaved: false,
|
|
580
|
-
};
|
|
581
|
-
initializeComponent([initial]);
|
|
582
|
-
expect(screen.getByRole('button', { name: /save/i })).not.toBeDisabled();
|
|
583
|
-
});
|
|
584
|
-
|
|
585
554
|
it('should render edit and delete icons in edit mode (renderedContent)', () => {
|
|
586
555
|
const button = {
|
|
587
556
|
index: 1,
|
|
@@ -19,6 +19,30 @@ import { sanitizeCardVarMappedValue } from './utils';
|
|
|
19
19
|
|
|
20
20
|
const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Assigns every variable token across the whole carousel a stable `cardVarMapped` key (a plain
|
|
24
|
+
* number, title then description, per card, in encounter order) — {cardIndex: {title: [key, …],
|
|
25
|
+
* description: [key, …]}}. Each occurrence is its own independent slot — even two tokens sharing
|
|
26
|
+
* the same literal tag name (e.g. "{{name}}" in both a card's title and description) get separate
|
|
27
|
+
* keys and are filled independently. A slot's key depends only on where its token sits in the
|
|
28
|
+
* template text, never on which tag currently fills it, so re-picking a different tag for the
|
|
29
|
+
* same slot never changes its key.
|
|
30
|
+
*/
|
|
31
|
+
export const computeCarouselVarSlotKeys = (carouselData, rcsVarRegex) => {
|
|
32
|
+
let nextNumber = 1;
|
|
33
|
+
return (carouselData || []).map((card = {}) => {
|
|
34
|
+
const keysForField = (fieldStr) => (fieldStr || '').match(rcsVarRegex)?.map(() => {
|
|
35
|
+
const key = String(nextNumber);
|
|
36
|
+
nextNumber += 1;
|
|
37
|
+
return key;
|
|
38
|
+
}) ?? [];
|
|
39
|
+
return {
|
|
40
|
+
title: keysForField(card.title),
|
|
41
|
+
description: keysForField(card.description),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
22
46
|
export const getCarouselImageAssetIndex = (cardIndex) =>
|
|
23
47
|
RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3);
|
|
24
48
|
export const getCarouselVideoAssetIndex = (cardIndex) =>
|
|
@@ -142,26 +166,22 @@ export const getCarouselDescriptionCharacterCount = (carouselData, cardIndex) =>
|
|
|
142
166
|
return descriptionText ? descriptionText.length : 0;
|
|
143
167
|
};
|
|
144
168
|
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Preview (TestAndPreviewSlidebox / listing preview): resolve each card's title/desc through
|
|
171
|
+
* `resolveCarouselTemplateWithMap(str, cardIndex, isDescField)` — each variable slot's key is its
|
|
172
|
+
* global position across the whole carousel (see `getCarouselGlobalVarSlotKey`), so two cards can
|
|
173
|
+
* never collide even when they pick the same tag name for their own slot.
|
|
174
|
+
*/
|
|
175
|
+
export const buildCarouselCardsForPreview = (cards = [], { isFullMode, resolveCarouselTemplateWithMap }) =>
|
|
176
|
+
(cards || []).map((card = {}, cardIndex) => {
|
|
152
177
|
const rawTitle = card.title || '';
|
|
153
178
|
const rawDesc = card.description || '';
|
|
154
|
-
const titleVarCount = (rawTitle.match(rcsVarRegex) || []).length;
|
|
155
|
-
const descVarCount = (rawDesc.match(rcsVarRegex) || []).length;
|
|
156
179
|
const resolvedTitle = !isFullMode
|
|
157
|
-
?
|
|
180
|
+
? resolveCarouselTemplateWithMap(rawTitle, cardIndex, false)
|
|
158
181
|
: rawTitle;
|
|
159
182
|
const resolvedDesc = !isFullMode
|
|
160
|
-
?
|
|
183
|
+
? resolveCarouselTemplateWithMap(rawDesc, cardIndex, true)
|
|
161
184
|
: rawDesc;
|
|
162
|
-
if (!isFullMode) {
|
|
163
|
-
slotOffset += titleVarCount + descVarCount;
|
|
164
|
-
}
|
|
165
185
|
const videoThumb = card.thumbnailSrc || card.videoAsset?.videoThumbnail || '';
|
|
166
186
|
const videoSrc = card.videoAsset?.videoSrc || '';
|
|
167
187
|
return {
|
|
@@ -174,7 +194,6 @@ export const buildCarouselCardsForPreview = (cards = [], { isFullMode, rcsVarReg
|
|
|
174
194
|
suggestions: card.suggestions || [],
|
|
175
195
|
};
|
|
176
196
|
});
|
|
177
|
-
};
|
|
178
197
|
|
|
179
198
|
/** Payload (create/edit RCS template): build `cardContent` for the carousel case. */
|
|
180
199
|
export const buildCarouselCardContentForPayload = (carouselData, {
|
|
@@ -182,43 +201,50 @@ export const buildCarouselCardContentForPayload = (carouselData, {
|
|
|
182
201
|
cardVarMapped,
|
|
183
202
|
selectedCarouselHeight,
|
|
184
203
|
rcsVarRegex,
|
|
185
|
-
}) =>
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
204
|
+
}) => {
|
|
205
|
+
const slotKeysByCard = computeCarouselVarSlotKeys(carouselData, rcsVarRegex);
|
|
206
|
+
return (carouselData || []).map((card = {}, cardIndex) => {
|
|
207
|
+
const cardMediaType = card.mediaType;
|
|
208
|
+
const isCardVideo = cardMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
209
|
+
const isCardImage = cardMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
210
|
+
const mediaUrl = isCardVideo
|
|
211
|
+
? (card.videoAsset?.videoSrc || '')
|
|
212
|
+
: (card.imageSrc || '');
|
|
213
|
+
const thumbnailUrl = isCardVideo
|
|
214
|
+
? (card.thumbnailSrc || card.videoAsset?.videoThumbnail || '')
|
|
215
|
+
: '';
|
|
216
|
+
const cardVarTokens = [
|
|
217
|
+
...((card.title || '').match(rcsVarRegex) ?? []),
|
|
218
|
+
...((card.description || '').match(rcsVarRegex) ?? []),
|
|
219
|
+
];
|
|
220
|
+
const cardSlotKeys = [
|
|
221
|
+
...(slotKeysByCard[cardIndex]?.title ?? []),
|
|
222
|
+
...(slotKeysByCard[cardIndex]?.description ?? []),
|
|
223
|
+
];
|
|
224
|
+
const cardVarMappedForCard = {};
|
|
225
|
+
cardVarTokens.forEach((token, tokenIndex) => {
|
|
202
226
|
const varName = getVarNameFromToken(token);
|
|
203
|
-
|
|
204
|
-
|
|
227
|
+
const slotKey = cardSlotKeys[tokenIndex];
|
|
228
|
+
if (!varName || !isSlotMappingMode) return;
|
|
229
|
+
const scopedValue = cardVarMapped?.[slotKey] ?? '';
|
|
230
|
+
cardVarMappedForCard[slotKey] = sanitizeCardVarMappedValue(scopedValue);
|
|
205
231
|
});
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
},
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
};
|
|
224
|
-
}
|
|
232
|
+
return {
|
|
233
|
+
title: card.title || '',
|
|
234
|
+
description: card.description || '',
|
|
235
|
+
mediaType: cardMediaType,
|
|
236
|
+
...((isCardImage || isCardVideo) && {
|
|
237
|
+
media: {
|
|
238
|
+
mediaUrl,
|
|
239
|
+
thumbnailUrl,
|
|
240
|
+
height: selectedCarouselHeight || MEDIUM,
|
|
241
|
+
...(isCardVideo && card.videoAsset?.videoName && { videoName: card.videoAsset.videoName }),
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
...(Array.isArray(card.suggestions) && card.suggestions.length > 0 && {
|
|
245
|
+
suggestions: card.suggestions,
|
|
246
|
+
}),
|
|
247
|
+
...(Object.keys(cardVarMappedForCard).length > 0 && { cardVarMapped: cardVarMappedForCard }),
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
};
|
|
@@ -15,6 +15,7 @@ import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
|
|
|
15
15
|
import TagList from '../../TagList';
|
|
16
16
|
import globalMessages from '../../Cap/messages';
|
|
17
17
|
import messages from '../messages';
|
|
18
|
+
import { RCS_TAG_AREA_FIELD_TITLE, RCS_TAG_AREA_FIELD_DESC } from '../constants';
|
|
18
19
|
import CarouselCardMedia from './CarouselCardMedia';
|
|
19
20
|
import CarouselCardButtons from './CarouselCardButtons';
|
|
20
21
|
import CarouselCharacterCount from './CarouselCharacterCount';
|
|
@@ -50,7 +51,8 @@ const CarouselCard = ({
|
|
|
50
51
|
onDescriptionChange,
|
|
51
52
|
onTitleAddVar,
|
|
52
53
|
onDescriptionAddVar,
|
|
53
|
-
|
|
54
|
+
onTitleTagSelect,
|
|
55
|
+
onDescTagSelect,
|
|
54
56
|
onContextChange,
|
|
55
57
|
renderEditMessage,
|
|
56
58
|
onFieldChange,
|
|
@@ -137,7 +139,7 @@ const CarouselCard = ({
|
|
|
137
139
|
(isEditFlow || !isFullMode) ? (
|
|
138
140
|
<TagList
|
|
139
141
|
label={formatMessage(globalMessages.addLabels)}
|
|
140
|
-
onTagSelect={
|
|
142
|
+
onTagSelect={onTitleTagSelect}
|
|
141
143
|
location={location}
|
|
142
144
|
tags={tagsForPicker}
|
|
143
145
|
onContextChange={onContextChange}
|
|
@@ -159,7 +161,7 @@ const CarouselCard = ({
|
|
|
159
161
|
/>
|
|
160
162
|
<CapRow className="rcs_text_area_wrapper">
|
|
161
163
|
{(isEditFlow || !isFullMode) ? (
|
|
162
|
-
renderEditMessage(card.title || '')
|
|
164
|
+
renderEditMessage(card.title || '', cardIndex, RCS_TAG_AREA_FIELD_TITLE)
|
|
163
165
|
) : (
|
|
164
166
|
<CapInput
|
|
165
167
|
value={card.title || ''}
|
|
@@ -189,7 +191,7 @@ const CarouselCard = ({
|
|
|
189
191
|
(isEditFlow || !isFullMode) ? (
|
|
190
192
|
<TagList
|
|
191
193
|
label={formatMessage(globalMessages.addLabels)}
|
|
192
|
-
onTagSelect={
|
|
194
|
+
onTagSelect={onDescTagSelect}
|
|
193
195
|
location={location}
|
|
194
196
|
tags={tagsForPicker}
|
|
195
197
|
onContextChange={onContextChange}
|
|
@@ -211,7 +213,7 @@ const CarouselCard = ({
|
|
|
211
213
|
/>
|
|
212
214
|
<CapRow className="rcs_text_area_wrapper">
|
|
213
215
|
{(isEditFlow || !isFullMode) ? (
|
|
214
|
-
renderEditMessage(card.description || '')
|
|
216
|
+
renderEditMessage(card.description || '', cardIndex, RCS_TAG_AREA_FIELD_DESC)
|
|
215
217
|
) : (
|
|
216
218
|
<TextArea
|
|
217
219
|
autosize={{ minRows: 3, maxRows: 5 }}
|
|
@@ -249,11 +251,6 @@ const CarouselCard = ({
|
|
|
249
251
|
hostName={hostName}
|
|
250
252
|
formatMessage={formatMessage}
|
|
251
253
|
onSuggestionsChange={onSuggestionsChange}
|
|
252
|
-
location={location}
|
|
253
|
-
tags={tagsForPicker}
|
|
254
|
-
injectedTags={injectedTags}
|
|
255
|
-
selectedOfferDetails={selectedOfferDetails}
|
|
256
|
-
onContextChange={onContextChange}
|
|
257
254
|
/>
|
|
258
255
|
</CapCard>
|
|
259
256
|
);
|
|
@@ -286,7 +283,8 @@ CarouselCard.propTypes = {
|
|
|
286
283
|
onDescriptionChange: PropTypes.func.isRequired,
|
|
287
284
|
onTitleAddVar: PropTypes.func.isRequired,
|
|
288
285
|
onDescriptionAddVar: PropTypes.func.isRequired,
|
|
289
|
-
|
|
286
|
+
onTitleTagSelect: PropTypes.func.isRequired,
|
|
287
|
+
onDescTagSelect: PropTypes.func.isRequired,
|
|
290
288
|
onContextChange: PropTypes.func,
|
|
291
289
|
renderEditMessage: PropTypes.func.isRequired,
|
|
292
290
|
onFieldChange: PropTypes.func.isRequired,
|
|
@@ -18,11 +18,6 @@ const CarouselCardButtons = ({
|
|
|
18
18
|
hostName,
|
|
19
19
|
formatMessage,
|
|
20
20
|
onSuggestionsChange,
|
|
21
|
-
location,
|
|
22
|
-
tags,
|
|
23
|
-
injectedTags,
|
|
24
|
-
selectedOfferDetails,
|
|
25
|
-
onContextChange,
|
|
26
21
|
}) => {
|
|
27
22
|
const suggestionsForCard = suggestions || [];
|
|
28
23
|
return (
|
|
@@ -73,11 +68,6 @@ const CarouselCardButtons = ({
|
|
|
73
68
|
host={hostName}
|
|
74
69
|
minSavedSuggestions={cardIndex === 0 ? 1 : 0}
|
|
75
70
|
hideDeleteSuggestionIndexes={cardIndex === 0 ? [0] : []}
|
|
76
|
-
location={location}
|
|
77
|
-
tags={tags}
|
|
78
|
-
injectedTags={injectedTags}
|
|
79
|
-
selectedOfferDetails={selectedOfferDetails}
|
|
80
|
-
onContextChange={onContextChange}
|
|
81
71
|
/>
|
|
82
72
|
</>
|
|
83
73
|
);
|
|
@@ -91,11 +81,6 @@ CarouselCardButtons.propTypes = {
|
|
|
91
81
|
hostName: PropTypes.string,
|
|
92
82
|
formatMessage: PropTypes.func.isRequired,
|
|
93
83
|
onSuggestionsChange: PropTypes.func.isRequired,
|
|
94
|
-
location: PropTypes.object,
|
|
95
|
-
tags: PropTypes.array,
|
|
96
|
-
injectedTags: PropTypes.object,
|
|
97
|
-
selectedOfferDetails: PropTypes.array,
|
|
98
|
-
onContextChange: PropTypes.func,
|
|
99
84
|
};
|
|
100
85
|
|
|
101
86
|
CarouselCardButtons.defaultProps = {
|
|
@@ -103,11 +88,6 @@ CarouselCardButtons.defaultProps = {
|
|
|
103
88
|
isEditFlow: false,
|
|
104
89
|
isFullMode: false,
|
|
105
90
|
hostName: '',
|
|
106
|
-
location: {},
|
|
107
|
-
tags: [],
|
|
108
|
-
injectedTags: {},
|
|
109
|
-
selectedOfferDetails: [],
|
|
110
|
-
onContextChange: () => {},
|
|
111
91
|
};
|
|
112
92
|
|
|
113
93
|
export default React.memo(CarouselCardButtons);
|