@capillarytech/creatives-library 9.0.56-alpha.5 → 9.0.56-alpha.6
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/templateVarUtils.js +32 -0
- package/utils/tests/templateVarUtils.test.js +44 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +17 -4
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +110 -2
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +3 -0
- package/v2Containers/CreativesContainer/index.js +1 -1
- package/v2Containers/CreativesContainer/tests/index.test.js +12 -0
- package/v2Containers/Whatsapp/index.js +8 -3
- package/v2Containers/mockdata.js +25 -0
package/package.json
CHANGED
|
@@ -115,6 +115,38 @@ export const extractTemplateVariables = (templateStr = '', captureRegex) => {
|
|
|
115
115
|
return variables;
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Reconciles a var-value map from an external source (e.g. a saved CommDefinition) into this
|
|
120
|
+
* UI's own `${token}_${segmentIndex}` slot-key format used by the WhatsApp/RCS editors' own
|
|
121
|
+
* text-merge logic.
|
|
122
|
+
*
|
|
123
|
+
* Two shapes are recognized:
|
|
124
|
+
* - Slot format: keys already look like `${token}_${index}` (contain an underscore) — used as-is.
|
|
125
|
+
* - CCS format: keys are plain sequential occurrence indices ("0", "1", ...), one per variable in
|
|
126
|
+
* template order — unrelated to this UI's own array-position indexing (which depends on how many
|
|
127
|
+
* plain-text segments fall between variables) — so it's remapped by walking `segments` in order
|
|
128
|
+
* and assigning the Nth variable occurrence to `rawVarMap[N]`.
|
|
129
|
+
*
|
|
130
|
+
* @param {Object} rawVarMap
|
|
131
|
+
* @param {string[]} segments - text+var segments, e.g. from `splitContentByOrderedVarTokens`
|
|
132
|
+
* @param {RegExp} regex - matches a variable token
|
|
133
|
+
* @returns {Object} `${token}_${segmentIndex}` -> value
|
|
134
|
+
*/
|
|
135
|
+
export const reconcileVarMapToSlotFormat = (rawVarMap = {}, segments = [], regex) => {
|
|
136
|
+
if (Object.keys(rawVarMap ?? {}).length === 0) return {};
|
|
137
|
+
const isSlotFormat = Object.keys(rawVarMap).some((key) => key.includes('_'));
|
|
138
|
+
if (isSlotFormat) return { ...rawVarMap };
|
|
139
|
+
const slotMap = {};
|
|
140
|
+
let occurrenceIndex = 0;
|
|
141
|
+
(segments ?? []).forEach((segment, segmentIndex) => {
|
|
142
|
+
if (typeof segment === 'string' && (segment.match(regex) || []).length > 0) {
|
|
143
|
+
slotMap[`${segment}_${segmentIndex}`] = rawVarMap[occurrenceIndex] ?? '';
|
|
144
|
+
occurrenceIndex += 1;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return slotMap;
|
|
148
|
+
};
|
|
149
|
+
|
|
118
150
|
/**
|
|
119
151
|
* Looks up the inner name of a `{{name}}` or `{#name#}` token in a flat key→value map.
|
|
120
152
|
* Handles both exact matches and dot-path suffixes (e.g. `tag.FORMAT_1` → name `FORMAT_1`).
|
|
@@ -6,8 +6,11 @@ import {
|
|
|
6
6
|
getFallbackResolvedContentForCardDisplay,
|
|
7
7
|
isDltHashVarToken,
|
|
8
8
|
isAnyTemplateVarToken,
|
|
9
|
+
reconcileVarMapToSlotFormat,
|
|
9
10
|
} from '../templateVarUtils';
|
|
10
11
|
|
|
12
|
+
const MUSTACHE_VAR_REGEX = /\{\{\d+\}\}/g;
|
|
13
|
+
|
|
11
14
|
describe('templateVarUtils', () => {
|
|
12
15
|
describe('splitContentByOrderedVarTokens', () => {
|
|
13
16
|
it('pushes remainder when next token is not found in string', () => {
|
|
@@ -201,4 +204,45 @@ describe('templateVarUtils', () => {
|
|
|
201
204
|
expect(getFallbackResolvedContent('{#a#}', {}, { a: '' })).toBe('{#a#}');
|
|
202
205
|
});
|
|
203
206
|
});
|
|
207
|
+
|
|
208
|
+
describe('reconcileVarMapToSlotFormat', () => {
|
|
209
|
+
it('returns an empty object for an empty/absent rawVarMap', () => {
|
|
210
|
+
expect(reconcileVarMapToSlotFormat({}, ['{{1}}'], MUSTACHE_VAR_REGEX)).toEqual({});
|
|
211
|
+
expect(reconcileVarMapToSlotFormat(undefined, ['{{1}}'], MUSTACHE_VAR_REGEX)).toEqual({});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('uses slot-format keys (containing an underscore) as-is', () => {
|
|
215
|
+
const rawVarMap = { '{{1}}_1': 'test', '{{2}}_3': 'test2' };
|
|
216
|
+
expect(
|
|
217
|
+
reconcileVarMapToSlotFormat(rawVarMap, ['x', '{{1}}', 'y', '{{2}}'], MUSTACHE_VAR_REGEX),
|
|
218
|
+
).toEqual(rawVarMap);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('remaps CCS-format sequential-occurrence-index keys ("0","1",...) onto this UI\'s own slot keys (regression: CCS sends plain occurrence indices, not this UI\'s `${token}_${arrayIndex}` format, which silently blanked WhatsApp edit fields and desynced the segment array)', () => {
|
|
222
|
+
// Mirrors the real CCS response: "Hi! Here is your latest tier progress summary with us.\n\nTo
|
|
223
|
+
// move up to the next tier, you still need to spend {{1}} more,\nearn {{2}} more points, ..."
|
|
224
|
+
// — varMapped keys "0".."10" map to {{1}}.."{{11}}" in template order.
|
|
225
|
+
const segments = [
|
|
226
|
+
'spend ',
|
|
227
|
+
'{{1}}',
|
|
228
|
+
' more,\nearn ',
|
|
229
|
+
'{{2}}',
|
|
230
|
+
' more points',
|
|
231
|
+
];
|
|
232
|
+
const rawVarMap = { 0: 'dasda', 1: 'dasd' };
|
|
233
|
+
expect(reconcileVarMapToSlotFormat(rawVarMap, segments, MUSTACHE_VAR_REGEX)).toEqual({
|
|
234
|
+
'{{1}}_1': 'dasda',
|
|
235
|
+
'{{2}}_3': 'dasd',
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('defaults a missing occurrence-index entry to an empty string rather than dropping the slot', () => {
|
|
240
|
+
const segments = ['a ', '{{1}}', ' b ', '{{2}}'];
|
|
241
|
+
const rawVarMap = { 0: 'filled' }; // no entry for occurrence index 1
|
|
242
|
+
expect(reconcileVarMapToSlotFormat(rawVarMap, segments, MUSTACHE_VAR_REGEX)).toEqual({
|
|
243
|
+
'{{1}}_1': 'filled',
|
|
244
|
+
'{{2}}_3': '',
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
204
248
|
});
|
|
@@ -64,6 +64,7 @@ const ChannelSelectionStep = ({
|
|
|
64
64
|
const [showIncentivesMenuMap, setShowIncentivesMenuMap] = useState({});
|
|
65
65
|
const [showTestAndPreview, setShowTestAndPreview] = useState(false);
|
|
66
66
|
const [testAndPreviewItem, setTestAndPreviewItem] = useState(null);
|
|
67
|
+
const [domainPropertiesData, setDomainPropertiesData] = useState(null);
|
|
67
68
|
const { formatMessage } = intl || {};
|
|
68
69
|
|
|
69
70
|
// Available channels (filter out hidden ones)
|
|
@@ -77,6 +78,19 @@ const ChannelSelectionStep = ({
|
|
|
77
78
|
const selectedChannelLowerCase = selectedChannel?.toLowerCase();
|
|
78
79
|
return CHANNELS.find((channel) => channel?.value?.toLowerCase() === selectedChannelLowerCase || channel?.channelProp === selectedChannelLowerCase) || null;
|
|
79
80
|
}, [selectedChannel]);
|
|
81
|
+
|
|
82
|
+
const editingZaloHostName = useMemo(() => {
|
|
83
|
+
if (!editingContentId) return '';
|
|
84
|
+
const editingItem = contentItems.find((c) => c.contentId === editingContentId);
|
|
85
|
+
if (editingItem?.channel?.toUpperCase() !== ZALO) return '';
|
|
86
|
+
const accountId = editingItem?.templateData?.accountId;
|
|
87
|
+
if (!accountId) return '';
|
|
88
|
+
const zaloDomains = domainPropertiesData?.ZALO || [];
|
|
89
|
+
const matchedDomain = zaloDomains.find(
|
|
90
|
+
(domain) => String(domain?.domainProperties?.connectionProperties?.oa_id) === String(accountId),
|
|
91
|
+
);
|
|
92
|
+
return matchedDomain?.domainProperties?.hostName || '';
|
|
93
|
+
}, [editingContentId, contentItems, domainPropertiesData]);
|
|
80
94
|
/**
|
|
81
95
|
* Handle CreativesContainer close
|
|
82
96
|
*/
|
|
@@ -467,6 +481,7 @@ const ChannelSelectionStep = ({
|
|
|
467
481
|
deliverySettingsData={deliverySettingsData}
|
|
468
482
|
deliverySetting={value?.deliverySetting}
|
|
469
483
|
onDeliverySettingChange={(deliverySetting) => onChange({ deliverySetting })}
|
|
484
|
+
onDomainPropertiesLoaded={setDomainPropertiesData}
|
|
470
485
|
intl={intl}
|
|
471
486
|
/>
|
|
472
487
|
)}
|
|
@@ -487,14 +502,12 @@ const ChannelSelectionStep = ({
|
|
|
487
502
|
getCreativesData={handleCreativesData}
|
|
488
503
|
handleCloseCreatives={handleCloseCreatives}
|
|
489
504
|
isFullMode={false}
|
|
505
|
+
hostName={editingZaloHostName}
|
|
490
506
|
messageDetails={{ type: 'default' }}
|
|
491
507
|
templateData={editingContentId ? (() => {
|
|
492
508
|
const saved = contentItems.find((c) => c.contentId === editingContentId)?.templateData;
|
|
493
|
-
// getTemplateData in CreativesContainer reads 'content', 'accountId', 'messageSubject' at
|
|
494
|
-
// top-level and needs 'type' for SlideBoxContent to set isEditWebPush. Our stored WEBPUSH
|
|
495
|
-
// templateData wraps those fields inside messageContent.content, so extract them here.
|
|
496
509
|
if (saved?.channel?.toUpperCase() === WEBPUSH && saved?.messageContent?.content) {
|
|
497
|
-
return { ...saved.messageContent.content, type: WEBPUSH };
|
|
510
|
+
return { ...saved.messageContent.content, type: WEBPUSH, channel: WEBPUSH };
|
|
498
511
|
}
|
|
499
512
|
return saved;
|
|
500
513
|
})() : null}
|
package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js
CHANGED
|
@@ -37,9 +37,17 @@ jest.mock('../../../../CreativesContainer', () => function MockCreativesContaine
|
|
|
37
37
|
handleCloseCreatives,
|
|
38
38
|
creativesMode,
|
|
39
39
|
channel,
|
|
40
|
+
templateData,
|
|
41
|
+
hostName,
|
|
40
42
|
}) {
|
|
41
43
|
return (
|
|
42
|
-
<div
|
|
44
|
+
<div
|
|
45
|
+
data-testid="creatives-mock"
|
|
46
|
+
data-creatives-mode={creativesMode}
|
|
47
|
+
data-creatives-channel={channel}
|
|
48
|
+
data-template-data={JSON.stringify(templateData)}
|
|
49
|
+
data-host-name={hostName}
|
|
50
|
+
>
|
|
43
51
|
<button
|
|
44
52
|
type="button"
|
|
45
53
|
data-testid="creatives-save"
|
|
@@ -62,7 +70,7 @@ jest.mock('../../../../CreativesContainer', () => function MockCreativesContaine
|
|
|
62
70
|
});
|
|
63
71
|
|
|
64
72
|
jest.mock('../../DeliverySettingsStep', () => ({
|
|
65
|
-
DeliverySettingsSection: function MockDeliverySettings({ onDeliverySettingChange }) {
|
|
73
|
+
DeliverySettingsSection: function MockDeliverySettings({ onDeliverySettingChange, onDomainPropertiesLoaded }) {
|
|
66
74
|
return (
|
|
67
75
|
<div data-testid="delivery-settings-section">
|
|
68
76
|
<button
|
|
@@ -72,6 +80,23 @@ jest.mock('../../DeliverySettingsStep', () => ({
|
|
|
72
80
|
>
|
|
73
81
|
Apply delivery
|
|
74
82
|
</button>
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
data-testid="domain-properties-loaded"
|
|
86
|
+
onClick={() => onDomainPropertiesLoaded?.({
|
|
87
|
+
ZALO: [{
|
|
88
|
+
id: 267284,
|
|
89
|
+
domainProperties: {
|
|
90
|
+
id: 4977,
|
|
91
|
+
domainName: 'Gapit_Automation',
|
|
92
|
+
connectionProperties: { oa_id: '300086756699856746' },
|
|
93
|
+
hostName: 'gapitzalotrans',
|
|
94
|
+
},
|
|
95
|
+
}],
|
|
96
|
+
})}
|
|
97
|
+
>
|
|
98
|
+
Load domain properties
|
|
99
|
+
</button>
|
|
75
100
|
</div>
|
|
76
101
|
);
|
|
77
102
|
},
|
|
@@ -1683,6 +1708,89 @@ describe('ChannelSelectionStep', () => {
|
|
|
1683
1708
|
expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-creatives-mode', 'edit');
|
|
1684
1709
|
});
|
|
1685
1710
|
|
|
1711
|
+
it('edit WEBPUSH item includes `channel: WEBPUSH` (not just `type`) in the unwrapped templateData (regression: CreativesContainer.getTemplateData\'s switch reads templateData.channel, not templateData.type — without it the switch never matched WEBPUSH and the edit screen rendered blank below its header)', async () => {
|
|
1712
|
+
renderStep(
|
|
1713
|
+
<ChannelSelectionStep
|
|
1714
|
+
value={{
|
|
1715
|
+
contentItems: [{
|
|
1716
|
+
contentId: 'wp-edit-channel',
|
|
1717
|
+
channel: 'WEBPUSH',
|
|
1718
|
+
templateData: {
|
|
1719
|
+
channel: 'WEBPUSH',
|
|
1720
|
+
messageContent: {
|
|
1721
|
+
content: { messageSubject: 'dasd', accountId: 13792, content: { title: 'dasd', message: 'dasd' } },
|
|
1722
|
+
},
|
|
1723
|
+
},
|
|
1724
|
+
}],
|
|
1725
|
+
}}
|
|
1726
|
+
onChange={jest.fn()}
|
|
1727
|
+
channels={CHANNELS}
|
|
1728
|
+
/>,
|
|
1729
|
+
);
|
|
1730
|
+
await userEvent.click(screen.getByLabelText('Show more content options icon'));
|
|
1731
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
|
|
1732
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('Edit'));
|
|
1733
|
+
await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
|
|
1734
|
+
const passedTemplateData = JSON.parse(screen.getByTestId('creatives-mock').getAttribute('data-template-data'));
|
|
1735
|
+
expect(passedTemplateData.channel).toBe('WEBPUSH');
|
|
1736
|
+
expect(passedTemplateData.type).toBe('WEBPUSH');
|
|
1737
|
+
expect(passedTemplateData.messageSubject).toBe('dasd');
|
|
1738
|
+
expect(passedTemplateData.accountId).toBe(13792);
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
// ── ZALO edit: hostName resolved from domainProperties ────────────────────────
|
|
1742
|
+
|
|
1743
|
+
it('resolves a Zalo item\'s hostName from the domainProperties fetch by matching accountId to connectionProperties.oa_id, and passes it to CreativesContainer (regression: CCS\'s zaloMessageContent never carries hostName, and without it Zalo/index.js\'s getTemplateInfoById guard never passes, so the edit view never loads live template data)', async () => {
|
|
1744
|
+
renderStep(
|
|
1745
|
+
<ChannelSelectionStep
|
|
1746
|
+
value={{
|
|
1747
|
+
contentItems: [{
|
|
1748
|
+
contentId: 'zalo-edit',
|
|
1749
|
+
channel: 'ZALO',
|
|
1750
|
+
templateData: {
|
|
1751
|
+
channel: 'ZALO',
|
|
1752
|
+
accountId: '300086756699856746',
|
|
1753
|
+
accountName: 'gapit_automation_account',
|
|
1754
|
+
token: 'zalo-token',
|
|
1755
|
+
templateConfigs: { id: '630142', name: '1592_Chí Linh_D1' },
|
|
1756
|
+
},
|
|
1757
|
+
}],
|
|
1758
|
+
}}
|
|
1759
|
+
onChange={jest.fn()}
|
|
1760
|
+
channels={CHANNELS}
|
|
1761
|
+
deliverySettingsData={{ required: false }}
|
|
1762
|
+
/>,
|
|
1763
|
+
);
|
|
1764
|
+
await userEvent.click(screen.getByTestId('domain-properties-loaded'));
|
|
1765
|
+
await userEvent.click(screen.getByLabelText('Show more content options icon'));
|
|
1766
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
|
|
1767
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('Edit'));
|
|
1768
|
+
await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
|
|
1769
|
+
expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-host-name', 'gapitzalotrans');
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
it('resolves an empty hostName for a Zalo item when no matching domainProperties account is loaded', async () => {
|
|
1773
|
+
renderStep(
|
|
1774
|
+
<ChannelSelectionStep
|
|
1775
|
+
value={{
|
|
1776
|
+
contentItems: [{
|
|
1777
|
+
contentId: 'zalo-edit-no-match',
|
|
1778
|
+
channel: 'ZALO',
|
|
1779
|
+
templateData: { channel: 'ZALO', accountId: 'unmatched-account-id' },
|
|
1780
|
+
}],
|
|
1781
|
+
}}
|
|
1782
|
+
onChange={jest.fn()}
|
|
1783
|
+
channels={CHANNELS}
|
|
1784
|
+
deliverySettingsData={{ required: false }}
|
|
1785
|
+
/>,
|
|
1786
|
+
);
|
|
1787
|
+
await userEvent.click(screen.getByLabelText('Show more content options icon'));
|
|
1788
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
|
|
1789
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('Edit'));
|
|
1790
|
+
await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
|
|
1791
|
+
expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-host-name', '');
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1686
1794
|
// ── FTP channel filtered from dropdown ───────────────────────────────────────
|
|
1687
1795
|
|
|
1688
1796
|
it('FTP channel is filtered out of the channel dropdown', async () => {
|
package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js
CHANGED
|
@@ -33,6 +33,7 @@ const DeliverySettingsSection = ({
|
|
|
33
33
|
deliverySettingsData,
|
|
34
34
|
deliverySetting = {},
|
|
35
35
|
onDeliverySettingChange,
|
|
36
|
+
onDomainPropertiesLoaded,
|
|
36
37
|
intl,
|
|
37
38
|
}) => {
|
|
38
39
|
const [showSlidebox, setShowSlidebox] = useState(false);
|
|
@@ -102,6 +103,7 @@ const DeliverySettingsSection = ({
|
|
|
102
103
|
)
|
|
103
104
|
: raw;
|
|
104
105
|
setDomainPropertiesData(entity);
|
|
106
|
+
onDomainPropertiesLoaded?.(entity);
|
|
105
107
|
}
|
|
106
108
|
} catch (err) {
|
|
107
109
|
if (!cancelled) setDomainPropertiesData(null);
|
|
@@ -316,6 +318,7 @@ DeliverySettingsSection.propTypes = {
|
|
|
316
318
|
deliverySettingsData: PropTypes.object,
|
|
317
319
|
deliverySetting: PropTypes.object,
|
|
318
320
|
onDeliverySettingChange: PropTypes.func,
|
|
321
|
+
onDomainPropertiesLoaded: PropTypes.func,
|
|
319
322
|
intl: PropTypes.object.isRequired,
|
|
320
323
|
};
|
|
321
324
|
|
|
@@ -412,7 +412,7 @@ export class Creatives extends React.Component {
|
|
|
412
412
|
this.setState({ isGetFormData: false });
|
|
413
413
|
};
|
|
414
414
|
|
|
415
|
-
mapCarouselDataToCreatives = (cards) => cards.map((card) => {
|
|
415
|
+
mapCarouselDataToCreatives = (cards) => (cards || []).map((card) => {
|
|
416
416
|
const {
|
|
417
417
|
cardVarMapped, bodyTemplate, media, buttons, mediaType,
|
|
418
418
|
} = card || {};
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
whatsappGetCreativeData2,
|
|
11
11
|
whatsappGetTemplateData1,
|
|
12
12
|
whatsappGetTemplateData2,
|
|
13
|
+
whatsappGetTemplateDataNullCards,
|
|
13
14
|
rcsTemplates,
|
|
14
15
|
rcsEditTemplateData,
|
|
15
16
|
smsEditTemplateData,
|
|
@@ -77,6 +78,17 @@ describe('Test SlideBoxContent container', () => {
|
|
|
77
78
|
expect(handleCloseCreatives).toHaveBeenCalledWith(true);
|
|
78
79
|
});
|
|
79
80
|
|
|
81
|
+
it('does not throw when templateConfigs.cards is explicitly null (regression: CCS sends null rather than omitting the field on non-carousel WhatsApp templates, and mapCarouselDataToCreatives called .map() straight off it)', () => {
|
|
82
|
+
expect(() =>
|
|
83
|
+
renderFunction(
|
|
84
|
+
'WHATSAPP',
|
|
85
|
+
'editTemplate',
|
|
86
|
+
whatsappTemplates,
|
|
87
|
+
whatsappGetTemplateDataNullCards,
|
|
88
|
+
),
|
|
89
|
+
).not.toThrow();
|
|
90
|
+
});
|
|
91
|
+
|
|
80
92
|
it('it should clear the url, on channel change from new whatsapp to another', () => {
|
|
81
93
|
renderFunction(
|
|
82
94
|
'WHATSAPP',
|
|
@@ -119,7 +119,7 @@ import { ANDROID } from '../../v2Components/CommonTestAndPreview/constants';
|
|
|
119
119
|
import CapImageUpload from '../../v2Components/CapImageUpload';
|
|
120
120
|
import TagList from '../TagList';
|
|
121
121
|
import { validateTags } from '../../utils/tagValidations';
|
|
122
|
-
import { splitContentByOrderedVarTokens } from '../../utils/templateVarUtils';
|
|
122
|
+
import { splitContentByOrderedVarTokens, reconcileVarMapToSlotFormat } from '../../utils/templateVarUtils';
|
|
123
123
|
import { capitalizeString } from '../../utils/Formatter';
|
|
124
124
|
import CapWhatsappCTA from '../../v2Components/CapWhatsappCTA';
|
|
125
125
|
import {
|
|
@@ -504,7 +504,10 @@ export const Whatsapp = (props) => {
|
|
|
504
504
|
if (templateHeaderArray?.length !== 0) {
|
|
505
505
|
let clonedVarMap = {};
|
|
506
506
|
if (!isEmpty(varMap)) {
|
|
507
|
-
|
|
507
|
+
// CCS's varMapped can use plain sequential occurrence indices ("0","1",...)
|
|
508
|
+
// rather than this UI's own `${token}_${index}` slot keys — reconcile so
|
|
509
|
+
// values land on the right segment instead of silently missing.
|
|
510
|
+
clonedVarMap = reconcileVarMapToSlotFormat(varMap, templateHeaderArray, regex);
|
|
508
511
|
} else {
|
|
509
512
|
templateHeaderArray?.forEach((headerValue, i) => {
|
|
510
513
|
if (headerValue?.match(regex)?.length > 0) {
|
|
@@ -562,7 +565,9 @@ export const Whatsapp = (props) => {
|
|
|
562
565
|
if (tempMsgArray.length !== 0) {
|
|
563
566
|
const { varMapped = {} } = editContent;
|
|
564
567
|
if (!isEmpty(varMapped)) {
|
|
565
|
-
|
|
568
|
+
// CCS's varMapped can use plain sequential occurrence indices ("0","1",...)
|
|
569
|
+
// rather than this UI's own `${token}_${index}` slot keys
|
|
570
|
+
varMap = reconcileVarMapToSlotFormat(varMapped, tempMsgArray, validVarRegex);
|
|
566
571
|
} else {
|
|
567
572
|
//computing and setting varMap for first edit
|
|
568
573
|
for (let i = 0; i < tempMsgArray.length; i += 1) {
|
package/v2Containers/mockdata.js
CHANGED
|
@@ -1111,6 +1111,31 @@ export default {
|
|
|
1111
1111
|
},
|
|
1112
1112
|
accountName: "WhatsappAccount",
|
|
1113
1113
|
},
|
|
1114
|
+
// CCS sends an explicit `cards: null` (not omitted) for a non-carousel WhatsApp
|
|
1115
|
+
// template — regression fixture for a crash reading `.map` off null.
|
|
1116
|
+
whatsappGetTemplateDataNullCards: {
|
|
1117
|
+
channel: "WHATSAPP",
|
|
1118
|
+
storeType: "REGISTERED_STORE",
|
|
1119
|
+
accountId: 12721,
|
|
1120
|
+
messagePartsCount: 1,
|
|
1121
|
+
messageBody: "Hey test, this is a plain WhatsApp template with no carousel.",
|
|
1122
|
+
templateConfigs: {
|
|
1123
|
+
name: "creatives_whatsapp6",
|
|
1124
|
+
language: "en",
|
|
1125
|
+
varMapped: {
|
|
1126
|
+
"{{1}}_1": "test",
|
|
1127
|
+
},
|
|
1128
|
+
template: "Hey {{1}}, this is a plain WhatsApp template with no carousel.",
|
|
1129
|
+
id: "creatives_whatsapp6",
|
|
1130
|
+
category: "MARKETING",
|
|
1131
|
+
buttonType: "NONE",
|
|
1132
|
+
buttons: null,
|
|
1133
|
+
mediaType: "TEXT",
|
|
1134
|
+
whatsappMedia: null,
|
|
1135
|
+
cards: null,
|
|
1136
|
+
},
|
|
1137
|
+
accountName: "WhatsappAccount",
|
|
1138
|
+
},
|
|
1114
1139
|
whatsappGetCreativeData1: {
|
|
1115
1140
|
value: {
|
|
1116
1141
|
name: "creatives_whatsapp6",
|