@capillarytech/creatives-library 8.0.239 → 8.0.241

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.239",
4
+ "version": "8.0.241",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -3,7 +3,10 @@ import React, { useState, useEffect, useCallback } from 'react';
3
3
  import { bindActionCreators } from 'redux';
4
4
  import { createStructuredSelector } from 'reselect';
5
5
  import { injectIntl, FormattedMessage } from 'react-intl';
6
- import { get, isEmpty, cloneDeep } from 'lodash';
6
+ import get from 'lodash/get';
7
+ import isEmpty from 'lodash/isEmpty';
8
+ import cloneDeep from 'lodash/cloneDeep';
9
+ import isNil from 'lodash/isNil';
7
10
  import styled from 'styled-components';
8
11
  import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
9
12
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
@@ -282,9 +285,22 @@ export const Rcs = (props) => {
282
285
  if (type === MESSAGE_TEXT) setTemplateDescError(false);
283
286
  return;
284
287
  }
288
+
289
+ let contentForValidation = resolved;
290
+ const placeholderTokens = templateStr.match(rcsVarRegex) || [];
291
+ placeholderTokens.forEach((t) => {
292
+ const escaped = t.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
293
+ contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
294
+ });
295
+ if (!contentForValidation.trim()) {
296
+ if (type === TITLE_TEXT) setTemplateTitleError(false);
297
+ if (type === MESSAGE_TEXT) setTemplateDescError(false);
298
+ return;
299
+ }
300
+
285
301
  const validationResponse =
286
302
  validateTags({
287
- content: resolved,
303
+ content: contentForValidation,
288
304
  tagsParam: tags,
289
305
  injectedTagsParams: injectedTags,
290
306
  location,
@@ -322,7 +338,8 @@ export const Rcs = (props) => {
322
338
  if (rcsVarTestRegex.test(elem)) {
323
339
  const key = getVarNameFromToken(elem);
324
340
  const v = cardVarMapped?.[key];
325
- return (v ?? '').toString();
341
+ if (isNil(v) || String(v)?.trim?.() === '') return elem;
342
+ return String(v);
326
343
  }
327
344
  return elem;
328
345
  }).join('');
@@ -330,7 +347,7 @@ export const Rcs = (props) => {
330
347
 
331
348
 
332
349
  useEffect(() => {
333
- if (isFullMode) return;
350
+ if (isFullMode || isEditFlow) return;
334
351
  const tokens = [
335
352
  ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
336
353
  ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
@@ -1729,8 +1746,7 @@ const splitTemplateVarString = (str) => {
1729
1746
  const currentDimension =selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
1730
1747
  return (
1731
1748
  <>
1732
- {renderLabel('cardOrientationLabel')}
1733
- {/* Match image behavior: allow changing dimensions only in full-mode create flow */}
1749
+ {isFullMode && renderLabel('cardOrientationLabel')}
1734
1750
  {!isEditFlow && isFullMode && (
1735
1751
  <CapSelect
1736
1752
  id="rcs-dimension-select"
@@ -1829,11 +1845,27 @@ const splitTemplateVarString = (str) => {
1829
1845
  if (!str) return '';
1830
1846
  if (!mapping || Object.keys(mapping).length === 0) return str;
1831
1847
  let result = str;
1832
- Object.keys(mapping).forEach((key) => {
1833
- const value = mapping[key];
1834
- if (!value) return;
1835
- const bracedValue = /^\{\{[\s\S]*\}\}$/.test(value) ? value : `{{${value}}}`;
1836
- const escaped = bracedValue.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
1848
+ const replacements = [];
1849
+ Object.entries(mapping).forEach(([key, value]) => {
1850
+ const raw = (value ?? '').toString();
1851
+ if (!raw || raw?.trim?.() === '') return;
1852
+ const braced = /^\{\{[\s\S]*\}\}$/.test(raw) ? raw : `{{${raw}}}`;
1853
+ replacements.push({ key, needle: raw });
1854
+ if (braced !== raw) replacements.push({ key, needle: braced });
1855
+ });
1856
+ const seen = new Set();
1857
+ const uniq = replacements
1858
+ .filter(({ key, needle }) => {
1859
+ const id = `${key}::${needle}`;
1860
+ if (seen.has(id)) return false;
1861
+ seen.add(id);
1862
+ return true;
1863
+ })
1864
+ .sort((a, b) => (b.needle.length - a.needle.length));
1865
+
1866
+ uniq.forEach(({ key, needle }) => {
1867
+ if (!needle) return;
1868
+ const escaped = needle.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
1837
1869
  const regex = new RegExp(escaped, 'g');
1838
1870
  result = result.replace(regex, `{{${key}}}`);
1839
1871
  });
@@ -1879,8 +1911,24 @@ const splitTemplateVarString = (str) => {
1879
1911
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType || MEDIUM
1880
1912
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType || MEDIUM,
1881
1913
  }}),
1882
- // ...(!isFullMode && {Status:RCS_STATUSES.approved}), //since, we anyways allow only approved templates to be sent, sending approved by default for edit functionality
1883
- ...(!isFullMode && { cardVarMapped }),
1914
+ ...(!isFullMode && (() => {
1915
+ const tokens = [
1916
+ ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
1917
+ ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
1918
+ ];
1919
+ const allowedKeys = tokens
1920
+ .map((t) => getVarNameFromToken(t))
1921
+ .filter(Boolean);
1922
+ const nextMap = {};
1923
+ allowedKeys.forEach((k) => {
1924
+ if (Object.prototype.hasOwnProperty.call(cardVarMapped || {}, k)) {
1925
+ nextMap[k] = cardVarMapped[k];
1926
+ } else {
1927
+ nextMap[k] = '';
1928
+ }
1929
+ });
1930
+ return { cardVarMapped: nextMap };
1931
+ })()),
1884
1932
  ...(suggestions.length > 0 && { suggestions }),
1885
1933
  }
1886
1934
  ],
@@ -968,6 +968,87 @@ describe('RCS createPayload', () => {
968
968
  expect(updatedTitleVarAreas.at(0).prop('value')).toBe('{{first_name}}');
969
969
  expect(updatedTitleVarAreas.at(1).prop('value')).toBe('{{first_name}}');
970
970
  });
971
+
972
+ it('should keep two tags + freetext inside the variable textarea in non-full mode edit (not merged into static text)', () => {
973
+ const templateData = {
974
+ name: 'TwoTagsAndFreeText',
975
+ versions: {
976
+ base: {
977
+ content: {
978
+ RCS: {
979
+ rcsContent: {
980
+ cardType: 'STANDALONE',
981
+ cardSettings: { cardOrientation: 'VERTICAL', cardWidth: 'SMALL' },
982
+ cardContent: [
983
+ {
984
+ // NOTE: In campaigns/edit flows, title/description can be stored "resolved" (values applied).
985
+ // We expect RCS to "unmap" this back to placeholders and show values in variable textareas.
986
+ title: 'Update for {{user_id_b64}} regarding {{first_name}}{{adv}}freeText',
987
+ description: 'Hi {{user_id_b64}}, your {{first_name}}{{adv}}freeText is ready.',
988
+ mediaType: 'NONE',
989
+ cardVarMapped: {
990
+ user_name: '{{user_id_b64}}',
991
+ service_type: '{{first_name}}{{adv}}freeText',
992
+ },
993
+ suggestions: [],
994
+ },
995
+ ],
996
+ contentType: 'RICHCARD',
997
+ },
998
+ },
999
+ },
1000
+ },
1001
+ },
1002
+ type: 'RCS',
1003
+ };
1004
+
1005
+ const wrapper = mountWithIntl(
1006
+ <Provider store={store}>
1007
+ <Rcs
1008
+ actions={{
1009
+ clearCreateResponse: jest.fn(),
1010
+ getTemplateDetails: jest.fn(),
1011
+ uploadRcsAsset: jest.fn(),
1012
+ clearRcsMediaAsset: jest.fn(),
1013
+ editTemplate: jest.fn(),
1014
+ clearEditResponse: jest.fn(),
1015
+ }}
1016
+ globalActions={{ fetchSchemaForEntity }}
1017
+ onCreateComplete={onCreateComplete}
1018
+ handleClose={handleClose}
1019
+ intl={{ formatMessage }}
1020
+ location={{ pathname: '/rcs/edit', query: { type: false, module: 'default' }, search: '' }}
1021
+ params={params}
1022
+ templateData={templateData}
1023
+ rcsData={{}}
1024
+ isFullMode={false}
1025
+ isEditFlow={false}
1026
+ loadingTags={false}
1027
+ metaEntities={[]}
1028
+ isDltEnabled={false}
1029
+ smsRegister={'DLT'}
1030
+ getFormData={jest.fn()}
1031
+ />
1032
+ </Provider>,
1033
+ );
1034
+
1035
+ wrapper.update();
1036
+
1037
+ // The placeholder {{service_type}} should exist as a variable textarea id, and its value should be the mixed string.
1038
+ const serviceTypeAreas = wrapper.find('TextArea').filterWhere((n) => {
1039
+ const id = n.prop('id') || '';
1040
+ return id.includes('{{service_type}}_');
1041
+ });
1042
+ expect(serviceTypeAreas.length).toBeGreaterThanOrEqual(1);
1043
+ expect(serviceTypeAreas.at(0).prop('value')).toBe('{{first_name}}{{adv}}freeText');
1044
+
1045
+ // Ensure freetext does NOT leak into static text blocks.
1046
+ const staticAreas = wrapper.find('TextArea').filterWhere((n) => !!n.prop('disabled'));
1047
+ const staticValues = staticAreas.map((n) => String(n.prop('value') || ''));
1048
+ staticValues.forEach((v) => {
1049
+ expect(v.includes('freeText')).toBe(false);
1050
+ });
1051
+ });
971
1052
  });
972
1053
 
973
1054
  describe('Character Counting Functions', () => {