@capillarytech/creatives-library 9.0.56-betaa.0 → 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.
Files changed (91) hide show
  1. package/constants/unified.js +7 -1
  2. package/package.json +1 -1
  3. package/services/api.js +25 -4
  4. package/utils/common.js +0 -4
  5. package/utils/templateVarUtils.js +16 -0
  6. package/utils/tests/templateVarUtils.test.js +44 -0
  7. package/v2Components/CapActionButton/index.js +73 -60
  8. package/v2Components/CapActionButton/index.scss +44 -28
  9. package/v2Components/CapActionButton/messages.js +7 -3
  10. package/v2Components/CapActionButton/tests/index.test.js +17 -1
  11. package/v2Components/CapWhatsappCTA/messages.js +4 -0
  12. package/v2Components/CapWhatsappCarouselButton/index.js +42 -33
  13. package/v2Components/CapWhatsappCarouselButton/index.scss +44 -2
  14. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +13 -0
  15. package/v2Components/CommonTestAndPreview/actions.js +13 -0
  16. package/v2Components/CommonTestAndPreview/constants.js +8 -0
  17. package/v2Components/CommonTestAndPreview/index.js +55 -3
  18. package/v2Components/CommonTestAndPreview/reducer.js +20 -0
  19. package/v2Components/CommonTestAndPreview/sagas.js +28 -0
  20. package/v2Components/CommonTestAndPreview/selectors.js +6 -0
  21. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +106 -0
  22. package/v2Components/CommonTestAndPreview/tests/actions.test.js +15 -0
  23. package/v2Components/CommonTestAndPreview/tests/constants.test.js +9 -1
  24. package/v2Components/CommonTestAndPreview/tests/index.test.js +101 -0
  25. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +41 -0
  26. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +84 -0
  27. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +12 -0
  28. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +36 -22
  29. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +5 -13
  30. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +6 -4
  31. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +13 -14
  32. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +15 -8
  33. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +31 -0
  34. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +13 -0
  35. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +26 -0
  36. package/v2Components/FormBuilder/_formBuilder.scss +8 -0
  37. package/v2Components/TestAndPreviewSlidebox/index.js +9 -0
  38. package/v2Containers/CommunicationFlow/CommunicationFlow.js +140 -75
  39. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +18 -2
  40. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +67 -28
  41. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +503 -51
  42. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +98 -4
  43. package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
  44. package/v2Containers/CommunicationFlow/constants.js +65 -4
  45. package/v2Containers/CommunicationFlow/index.js +16 -23
  46. package/v2Containers/CommunicationFlow/messages.js +11 -3
  47. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +27 -11
  48. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +4 -0
  49. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +176 -4
  50. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +22 -4
  51. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +14 -4
  52. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +11 -0
  53. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +51 -0
  54. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +11 -4
  55. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +19 -5
  56. package/v2Containers/CreativesContainer/SlideBoxContent.js +15 -0
  57. package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
  58. package/v2Containers/CreativesContainer/index.js +29 -3
  59. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +29 -0
  60. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +18 -5
  61. package/v2Containers/CreativesContainer/tests/index.test.js +12 -0
  62. package/v2Containers/MobilePush/Create/index.js +18 -1
  63. package/v2Containers/MobilePush/Create/test/contentValidity.test.js +96 -0
  64. package/v2Containers/MobilePush/Edit/index.js +18 -1
  65. package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +116 -0
  66. package/v2Containers/MobilePush/commonMethods.js +49 -1
  67. package/v2Containers/MobilePushNew/index.js +29 -4
  68. package/v2Containers/MobilePushNew/tests/index.test.js +119 -0
  69. package/v2Containers/MobilePushNew/tests/utils.test.js +82 -0
  70. package/v2Containers/MobilePushNew/utils.js +34 -1
  71. package/v2Containers/MobilepushWrapper/index.js +3 -1
  72. package/v2Containers/Rcs/index.js +34 -0
  73. package/v2Containers/Rcs/index.scss +15 -0
  74. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +115 -19
  75. package/v2Containers/Rcs/tests/index.test.js +67 -0
  76. package/v2Containers/Sms/Create/index.js +3 -1
  77. package/v2Containers/Sms/Edit/index.js +25 -0
  78. package/v2Containers/Sms/Edit/tests/index.test.js +85 -0
  79. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +27 -3
  80. package/v2Containers/Viber/index.js +24 -1
  81. package/v2Containers/Viber/tests/index.test.js +103 -0
  82. package/v2Containers/WebPush/Create/index.js +19 -0
  83. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +17 -12
  84. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +5 -0
  85. package/v2Containers/WebPush/Create/tests/contentValidity.test.js +294 -0
  86. package/v2Containers/Whatsapp/index.js +25 -4
  87. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +306 -34
  88. package/v2Containers/Whatsapp/tests/index.test.js +115 -0
  89. package/v2Containers/Zalo/index.js +28 -1
  90. package/v2Containers/Zalo/tests/index.test.js +99 -0
  91. package/v2Containers/mockdata.js +25 -0
@@ -64,7 +64,6 @@ export const ENABLE_AI_SUGGESTIONS = 'ENABLE_AI_SUGGESTIONS';
64
64
  export const AI_CONTENT_BOT_DISABLED = 'AI_CONTENT_BOT_DISABLED';
65
65
  export const AI_DOCUMENTATION_BOT_ENABLED = 'AI_DOCUMENTATION_BOT_ENABLED';
66
66
  export const ENABLE_PRODUCT_SUPPORT_VIDEOS = 'ENABLE_PRODUCT_SUPPORT_VIDEOS';
67
- export const SUPPORT_ENGAGEMENT_MODULE = 'SUPPORT_ENGAGEMENT_MODULE';
68
67
  export const EMBEDDED = 'embedded';
69
68
  // --- Tag/Validation Constants ---
70
69
  export const CARD_RELATED_TAGS = [
@@ -230,3 +229,10 @@ export const RCS_API_SMS_FALLBACK_KEYS = Object.freeze({
230
229
  RCS_SMS_FALLBACK_VAR_MAPPED: 'rcsSmsFallbackVarMapped',
231
230
  });
232
231
 
232
+ export const HTTP_METHODS = Object.freeze({
233
+ GET: 'GET',
234
+ POST: 'POST',
235
+ PUT: 'PUT',
236
+ PATCH: 'PATCH',
237
+ DELETE: 'DELETE',
238
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.56-betaa.0",
4
+ "version": "9.0.57-alpha.0",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/services/api.js CHANGED
@@ -12,6 +12,7 @@ import getSchema from './getSchema';
12
12
  import * as API from '../utils/ApiCaller';
13
13
  import { addBaseToTemplate, transformCustomFieldsData } from '../utils/commonUtils';
14
14
  import { EMAIL, SMS, TRANSACTION } from '../v2Containers/CreativesContainer/constants';
15
+ import { HTTP_METHODS } from '../constants/unified';
15
16
  let API_ENDPOINT = config.development.api_endpoint;
16
17
  let EXPORT_API_ENDPOINT = config.development.exports_api_endpoint;
17
18
  let CAMPAIGNS_API_ENDPOINT = config.development.campaigns_api_endpoint;
@@ -698,22 +699,42 @@ export const getLiquidTags = (content) => {
698
699
 
699
700
  export const createCentralCommsMetaId = (payload, metaType = TRANSACTION) => {
700
701
  const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}`;
701
- return request(url, getAPICallObject('POST', payload, false, false, false, true));
702
+ return request(url, getAPICallObject(HTTP_METHODS.POST, payload, false, false, false, true));
703
+ };
704
+
705
+ // Use the CCS CommDefinition API for CommunicationFlow Save; keep legacy messageMeta APIs for CreativesContainer.
706
+ const COMM_DEFINITIONS_PATH = `${API_ENDPOINT}/comm-definitions`;
707
+
708
+ export const createCommDefinition = (payload) => {
709
+ const url = COMM_DEFINITIONS_PATH;
710
+ return request(url, getAPICallObject(HTTP_METHODS.POST, payload, false, false, false, true));
711
+ };
712
+
713
+ // Opens a new DRAFT version on an existing CommDefinition, preserving its id/referenceId.
714
+ export const editCommDefinition = (commDefinitionId, payload) => {
715
+ const url = `${COMM_DEFINITIONS_PATH}/${commDefinitionId}/edit`;
716
+ return request(url, getAPICallObject(HTTP_METHODS.POST, payload, false, false, false, true));
717
+ };
718
+
719
+ // notify/ui's "Send test" uses this instead of the legacy createMessageMeta/sendTestMessage flow.
720
+ export const sendTransactionalComm = (payload) => {
721
+ const url = `${COMM_DEFINITIONS_PATH}/send/transaction`;
722
+ return request(url, getAPICallObject(HTTP_METHODS.POST, payload, false, false, false, true));
702
723
  };
703
724
 
704
725
  export const getCentralCommsMetaIds = (metaIds, metaType = TRANSACTION) => {
705
726
  const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}?metaIds=${metaIds}`;
706
- return request(url, getAPICallObject('GET', null, false, false, false, true));
727
+ return request(url, getAPICallObject(HTTP_METHODS.GET, null, false, false, false, true));
707
728
  };
708
729
 
709
730
  export const bulkClaimAndApprove = (payload, metaType = TRANSACTION) => {
710
731
  const url = `${API_ENDPOINT}/common/central-comms/bulk-claim-approve/${metaType}`;
711
- return request(url, getAPICallObject('POST', payload, false, false, false, true));
732
+ return request(url, getAPICallObject(HTTP_METHODS.POST, payload, false, false, false, true));
712
733
  };
713
734
 
714
735
  export const updateMetaConfig = (payload, metaType = TRANSACTION, metaId) => {
715
736
  const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}/${metaId}`;
716
- return request(url, getAPICallObject('PATCH', payload, false, false, false, true));
737
+ return request(url, getAPICallObject(HTTP_METHODS.PATCH, payload, false, false, false, true));
717
738
  };
718
739
 
719
740
  // Customer Search APIs for Email Test & Preview Feature
package/utils/common.js CHANGED
@@ -100,10 +100,6 @@ export const hasSupportCKEditor = Auth.hasFeatureAccess.bind(
100
100
  null,
101
101
  SUPPORT_CK_EDITOR,
102
102
  );
103
- export const hasSupportEngagementModule = Auth.hasFeatureAccess.bind(
104
- null,
105
- SUPPORT_ENGAGEMENT_MODULE,
106
- );
107
103
 
108
104
  export const hasGiftVoucherFeature = Auth.hasFeatureAccess.bind(
109
105
  null,
@@ -115,6 +115,22 @@ export const extractTemplateVariables = (templateStr = '', captureRegex) => {
115
115
  return variables;
116
116
  };
117
117
 
118
+ // Reconcile slot-format (`${token}_${index}`) and CCS-format (`"0"`, `"1"`, ...) var maps into the editor's `${token}_${segmentIndex}` slot-key format.
119
+ export const reconcileVarMapToSlotFormat = (rawVarMap = {}, segments = [], regex) => {
120
+ if (Object.keys(rawVarMap ?? {}).length === 0) return {};
121
+ const isSlotFormat = Object.keys(rawVarMap).some((key) => key.includes('_'));
122
+ if (isSlotFormat) return { ...rawVarMap };
123
+ const slotMap = {};
124
+ let occurrenceIndex = 0;
125
+ (segments ?? []).forEach((segment, segmentIndex) => {
126
+ if (typeof segment === 'string' && (segment.match(regex) || []).length > 0) {
127
+ slotMap[`${segment}_${segmentIndex}`] = rawVarMap[occurrenceIndex] ?? '';
128
+ occurrenceIndex += 1;
129
+ }
130
+ });
131
+ return slotMap;
132
+ };
133
+
118
134
  /**
119
135
  * Looks up the inner name of a `{{name}}` or `{#name#}` token in a flat key→value map.
120
136
  * 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
  });
@@ -101,10 +101,12 @@ export const CapActionButton = (props) => {
101
101
  const onButtonTextChange = ({target}) => {
102
102
  const { value, id } = target;
103
103
  let errorMessage = '';
104
- if (value.length > BTN_MAX_LENGTH) {
104
+ if (!value.trim()) {
105
+ errorMessage = formatMessage(messages.ctaButtonTextEmptyMessage);
106
+ } else if (!isValidText(value)) {
107
+ errorMessage = formatMessage(messages.ctaButtonTextErrorMessage);
108
+ } else if (value.length > BTN_MAX_LENGTH) {
105
109
  errorMessage = formatMessage(messages.ctaButtonTextLengthError, { maxLength: BTN_MAX_LENGTH });
106
- } if (!isValidText(value)) {
107
- errorMessage = formatMessage(messages.ctaButtonErrorMessage);
108
110
  }
109
111
  setButtonError(errorMessage);
110
112
  updateDisplayAndPostback(value, id);
@@ -446,12 +448,11 @@ export const CapActionButton = (props) => {
446
448
  gutter={9}
447
449
  >
448
450
  <CapColumn
449
- span={1}
450
- className={`${ctaIsPhone ? 'whatsapp-saved-cta-phone-icon' : ''}`}
451
+ className={`rcs-saved-cta-cell ${ctaIsPhone ? 'whatsapp-saved-cta-phone-icon' : ''}`}
451
452
  >
452
453
  <CapIcon size="s" type={ctaIsPhone ? 'call' : (ctaIsReply ? 'small-link' : 'open-in-new')} />
453
454
  </CapColumn>
454
- <CapColumn span={6}>
455
+ <CapColumn className="rcs-saved-cta-cell rcs-saved-cta-text-cell">
455
456
  <CapLabel
456
457
  type="label2"
457
458
  className="rcs-saved-cta-button-text"
@@ -459,64 +460,76 @@ export const CapActionButton = (props) => {
459
460
  {text}
460
461
  </CapLabel>
461
462
  </CapColumn>
462
- <CapColumn flex={1} className="rcs-saved-cta-value-column">
463
- <div className="rcs-saved-cta-value-actions">
464
- {ctaIsPhone && (
465
- <CapLabel className="phone">+{phoneNumber}</CapLabel>
466
- )}
467
- {!ctaIsPhone && !ctaIsReply && (
468
- <>
469
- <CapLabel className="url-type" type="label2">
470
- {(cta.urlType || RCS_CTA_URL_TYPE.STATIC) === RCS_CTA_URL_TYPE.DYNAMIC
471
- ? formatMessage(messages.ctaWebsiteTypeDynamic)
472
- : formatMessage(messages.ctaWebsiteTypeStatic)}
473
- </CapLabel>
474
- <CapTooltip title={url} placement="top">
475
- <CapLabel className="url">{url}</CapLabel>
476
- </CapTooltip>
477
- </>
478
- )}
479
- {isDynamicUrlSubtype && (url || '').includes(DYNAMIC_URL_SUFFIX) && (
480
- <TagList
481
- className="rcs-cta-taglist"
482
- label={formatMessage(messages.ctaTagListLabel)}
483
- onTagSelect={(data) => onTagSelect(data, index, url)}
484
- location={location}
485
- tags={tags}
486
- injectedTags={injectedTags}
487
- selectedOfferDetails={selectedOfferDetails}
488
- onContextChange={onContextChange}
463
+ <CapColumn className="rcs-saved-cta-cell rcs-saved-cta-type-cell">
464
+ {!ctaIsPhone && !ctaIsReply && (
465
+ <CapLabel className="url-type" type="label2">
466
+ {(cta.urlType || RCS_CTA_URL_TYPE.STATIC) === RCS_CTA_URL_TYPE.DYNAMIC
467
+ ? formatMessage(messages.ctaWebsiteTypeDynamic)
468
+ : formatMessage(messages.ctaWebsiteTypeStatic)}
469
+ </CapLabel>
470
+ )}
471
+ </CapColumn>
472
+ <CapColumn className="rcs-saved-cta-cell rcs-saved-cta-url-cell">
473
+ {ctaIsPhone && (
474
+ <CapLabel className="phone">+{phoneNumber}</CapLabel>
475
+ )}
476
+ {!ctaIsPhone && !ctaIsReply && (
477
+ <CapTooltip
478
+ title={url}
479
+ placement="top"
480
+ wrapperClassName="rcs-cta-url-tooltip-wrapper"
481
+ triggerClassName="rcs-cta-url-tooltip-trigger"
482
+ >
483
+ <CapLabel className="url">{url}</CapLabel>
484
+ </CapTooltip>
485
+ )}
486
+ </CapColumn>
487
+ <CapColumn className="rcs-saved-cta-cell rcs-saved-cta-actions-cell">
488
+ {isDynamicUrlSubtype && (url || '').includes(DYNAMIC_URL_SUFFIX) && (
489
+ <TagList
490
+ className="rcs-cta-taglist"
491
+ label={formatMessage(messages.ctaTagListLabel)}
492
+ onTagSelect={(data) => onTagSelect(data, index, url)}
493
+ location={location}
494
+ tags={tags}
495
+ injectedTags={injectedTags}
496
+ selectedOfferDetails={selectedOfferDetails}
497
+ onContextChange={onContextChange}
498
+ />
499
+ )}
500
+ {isDynamicUrlSubtype && !(url || '').includes(DYNAMIC_URL_SUFFIX) && (
501
+ <CapTooltip title={formatMessage(messages.ctaTagListRevert)} placement="top">
502
+ <CapIcon
503
+ size="s"
504
+ type="return"
505
+ className="rcs-cta-tag-revert"
506
+ onClick={() => revertTagSelect(index, url)}
489
507
  />
490
- )}
491
- {isDynamicUrlSubtype && !(url || '').includes(DYNAMIC_URL_SUFFIX) && (
492
- <CapTooltip title={formatMessage(messages.ctaTagListRevert)} placement="top">
493
- <CapIcon
494
- size="s"
495
- type="return"
496
- className="rcs-cta-tag-revert"
497
- onClick={() => revertTagSelect(index, url)}
498
- />
499
- </CapTooltip>
500
- )}
501
- {(isFullMode && !isEditFlow) && (
502
- <div className="rcs-saved-cta-action-icons">
508
+ </CapTooltip>
509
+ )}
510
+ {(isFullMode && !isEditFlow) && (
511
+ <CapRow
512
+ className="rcs-saved-cta-action-icons"
513
+ type="flex"
514
+ align="middle"
515
+ noWrap
516
+ >
517
+ <CapIcon
518
+ size="s"
519
+ type="edit"
520
+ className="rcs-saved-cta-edit-icon"
521
+ onClick={() => editCta(index)}
522
+ />
523
+ {!(isHostIcs && index === 0) && !hideDeleteForSuggestionIndex(index) && !cannotDeleteSavedMandatory(index) && (
503
524
  <CapIcon
504
525
  size="s"
505
- type="edit"
506
- className="rcs-saved-cta-edit-icon"
507
- onClick={() => editCta(index)}
526
+ type="delete"
527
+ className="rcs-saved-cta-delete-icon"
528
+ onClick={() => deleteButtonHandler(index)}
508
529
  />
509
- {!(isHostIcs && index === 0) && !hideDeleteForSuggestionIndex(index) && !cannotDeleteSavedMandatory(index) && (
510
- <CapIcon
511
- size="s"
512
- type="delete"
513
- className="rcs-saved-cta-delete-icon"
514
- onClick={() => deleteButtonHandler(index)}
515
- />
516
- )}
517
- </div>
518
- )}
519
- </div>
530
+ )}
531
+ </CapRow>
532
+ )}
520
533
  </CapColumn>
521
534
  </CapRow>
522
535
  );
@@ -25,37 +25,51 @@
25
25
  margin-bottom: $CAP_SPACE_12;
26
26
  flex-wrap: nowrap;
27
27
 
28
- // Icon/text columns keep a fixed span; this column takes whatever space
29
- // is left (flex: 1, set via CapColumn's `flex` prop) instead of a fixed
30
- // span that has to add up to exactly 24 alongside its siblings — with a
31
- // fixed span, the gutter/margin between columns had nowhere to come from
32
- // and pushed this column's content past the card's right border.
33
- .rcs-saved-cta-value-column {
28
+ .rcs-saved-cta-cell {
29
+ display: flex;
30
+ align-items: center;
34
31
  min-width: 0;
32
+
33
+ // Cell children are flex items too — without this their own min-content
34
+ // (the full text/URL) makes the cell unshrinkable and pushes the row.
35
+ > * {
36
+ min-width: 0;
37
+ }
35
38
  }
36
39
 
37
- // Value text and edit/delete icons share one flex row, right-aligned as a
38
- // unit — keeps the value flush against the icons regardless of its length,
39
- // instead of the old fixed-span column + absolutely-positioned icon combo
40
- // (which drifted out of alignment with variable-length URLs/numbers).
41
- .rcs-saved-cta-value-actions {
42
- display: flex;
43
- align-items: center;
40
+ // Fixed basis for the 20-char button text cap.
41
+ .rcs-saved-cta-text-cell {
42
+ flex: 0 1 10rem;
43
+ }
44
+
45
+ // Fixed, not auto: "Static" is narrower than "Dynamic", which left the URL
46
+ // starting at a different x per row.
47
+ .rcs-saved-cta-type-cell {
48
+ flex: 0 1 4rem;
49
+ }
50
+
51
+ .rcs-saved-cta-url-cell {
52
+ flex: 1 1 0;
53
+ }
54
+
55
+ .rcs-saved-cta-actions-cell {
56
+ flex: 0 0 auto;
44
57
  justify-content: flex-end;
45
58
  gap: $CAP_SPACE_08;
46
- width: 100%;
47
- min-width: 0;
59
+ }
48
60
 
49
- .url-type {
50
- flex-shrink: 0;
51
- }
61
+ // CapTooltip nests `.url` two levels down, so these — not `.url` — are the
62
+ // boxes that must be allowed to shrink for the ellipsis to apply.
63
+ .rcs-cta-url-tooltip-wrapper {
64
+ flex: 1 1 0;
65
+ min-width: 0;
66
+ }
52
67
 
53
- .url, .phone {
54
- min-width: 0;
55
- }
68
+ .rcs-cta-url-tooltip-trigger {
69
+ min-width: 0;
56
70
  }
57
71
 
58
- .url {
72
+ .url, .rcs-saved-cta-button-text, .url-type {
59
73
  white-space: nowrap;
60
74
  overflow: hidden;
61
75
  text-overflow: ellipsis;
@@ -69,10 +83,6 @@
69
83
  word-break: break-word;
70
84
  }
71
85
 
72
- .saved-button-text {
73
- font-weight: 500;
74
- }
75
-
76
86
  .rcs-saved-cta-action-icons {
77
87
  display: flex;
78
88
  align-items: center;
@@ -85,8 +95,6 @@
85
95
  cursor: pointer;
86
96
  }
87
97
 
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
98
  .rcs-cta-tag-revert {
91
99
  display: flex;
92
100
  align-items: center;
@@ -95,7 +103,15 @@
95
103
  }
96
104
 
97
105
  .rcs-cta-taglist {
106
+ display: flex;
107
+ align-items: center;
98
108
  flex-shrink: 0;
109
+
110
+ // CapTagList floats this trigger right, taking it out of flow, which
111
+ // let it paint over the edit icon instead of occupying real width.
112
+ .tooltip-add-label-container {
113
+ float: none;
114
+ }
99
115
  }
100
116
  }
101
117
 
@@ -83,9 +83,13 @@ export default defineMessages({
83
83
  id: `${prefix}.comingSoonTooltip`,
84
84
  defaultMessage: 'Right now only one Call to action button is enabled, in future you would be able to add upto 3 buttons.',
85
85
  },
86
- ctaButtonErrorMessage: {
87
- id: `${prefix}.ctaButtonErrorMessage`,
88
- defaultMessage: 'Button text cannot be empty',
86
+ ctaButtonTextEmptyMessage: {
87
+ id: `${prefix}.ctaButtonTextEmptyMessage`,
88
+ defaultMessage: 'Button text cannot be empty or contain only whitespace',
89
+ },
90
+ ctaButtonTextErrorMessage: {
91
+ id: `${prefix}.ctaButtonTextErrorMessage`,
92
+ defaultMessage: 'Newlines, Emojis, or Special characters are not allowed',
89
93
  },
90
94
  ctaButtonTextLengthError: {
91
95
  id: `${prefix}.ctaButtonTextLengthError`,
@@ -521,7 +521,23 @@ describe('CapActionButton', () => {
521
521
  initializeComponent([initial]);
522
522
  const buttontextInput = screen.getByPlaceholderText('Enter button text');
523
523
  fireEvent.change(buttontextInput, { target: { value: '&*^', id: 0 } });
524
- expect(screen.getByText(/button text cannot be empty/i)).toBeInTheDocument();
524
+ expect(screen.getByText(/special characters are not allowed/i)).toBeInTheDocument();
525
+ });
526
+
527
+ it('should show empty error for whitespace-only button text (onButtonTextChange)', () => {
528
+ const initial = {
529
+ index: 0,
530
+ type: RCS_BUTTON_TYPES.QUICK_REPLY,
531
+ text: 'Shop',
532
+ phoneNumber: '',
533
+ url: '',
534
+ postback: 'Shop',
535
+ isSaved: false,
536
+ };
537
+ initializeComponent([initial]);
538
+ const buttontextInput = screen.getByPlaceholderText('Enter button text');
539
+ fireEvent.change(buttontextInput, { target: { value: ' ', id: 0 } });
540
+ expect(screen.getByText(/cannot be empty or contain only whitespace/i)).toBeInTheDocument();
525
541
  });
526
542
 
527
543
  it('should show error for invalid url (onUrlChange)', () => {
@@ -57,6 +57,10 @@ export default defineMessages({
57
57
  id: `${prefix}.ctaButtonErrorMessage`,
58
58
  defaultMessage: "Newlines, Emojis, or Special characters are not allowed",
59
59
  },
60
+ ctaButtonTextEmptyMessage: {
61
+ id: `${prefix}.ctaButtonTextEmptyMessage`,
62
+ defaultMessage: 'Button text cannot be empty or contain only whitespace',
63
+ },
60
64
  addButton: {
61
65
  id: `${prefix}.addButton`,
62
66
  defaultMessage: 'Add button',
@@ -126,7 +126,9 @@ export const CapWhatsappCarouselButton = (props) => {
126
126
 
127
127
  const onButtonTextChange = (value, buttonIndex) => {
128
128
  let errorMessage = '';
129
- if (!isValidWhatsappCtaText(value)) {
129
+ if (!value.trim()) {
130
+ errorMessage = formatMessage(capWhatsappCTAMsg.ctaButtonTextEmptyMessage);
131
+ } else if (!isValidWhatsappCtaText(value)) {
130
132
  errorMessage = formatMessage(capWhatsappCTAMsg.ctaButtonErrorMessage);
131
133
  }
132
134
  onValueChange(buttonIndex, [{fieldName: 'text', value}, {fieldName: 'textError', value: errorMessage}]);
@@ -333,12 +335,11 @@ export const CapWhatsappCarouselButton = (props) => {
333
335
  noWrap
334
336
  >
335
337
  <CapColumn
336
- span={1}
337
- className={`${isPhoneNumberType ? 'whatsapp-saved-cta-phone-icon' : ''}`}
338
+ className={`whatsapp-saved-cta-cell ${isPhoneNumberType ? 'whatsapp-saved-cta-phone-icon' : ''}`}
338
339
  >
339
340
  <CapIcon size="s" type={isPhoneNumberType ? 'call' : isURLtype ? 'launch' : 'six-dots'} />
340
341
  </CapColumn>
341
- <CapColumn span={6}>
342
+ <CapColumn className="whatsapp-saved-cta-cell whatsapp-saved-cta-text-cell">
342
343
  <CapLabel
343
344
  type="label2"
344
345
  className="whatsapp-saved-cta-button-text"
@@ -346,39 +347,47 @@ export const CapWhatsappCarouselButton = (props) => {
346
347
  {button?.text}
347
348
  </CapLabel>
348
349
  </CapColumn>
349
- {isPhoneNumberType && (
350
- <CapColumn span={10} align="left">
350
+ <CapColumn className="whatsapp-saved-cta-cell whatsapp-saved-cta-type-cell">
351
+ {isURLtype && (
352
+ <CapLabel className="url-type" type="label2">
353
+ {button?.urlType === STATIC_URL
354
+ ? formatMessage(capWhatsappCTAMsg.ctaWebsiteTypeStatic)
355
+ : formatMessage(capWhatsappCTAMsg.ctaWebsiteTypeDynamic)}
356
+ </CapLabel>
357
+ )}
358
+ </CapColumn>
359
+ <CapColumn className="whatsapp-saved-cta-cell whatsapp-saved-cta-url-cell whatsapp-url-preview-wrapper">
360
+ {isPhoneNumberType && (
351
361
  <CapLabel className="phone">
352
362
  {button?.phone_number}
353
363
  </CapLabel>
354
- </CapColumn>
355
- )}
356
- {isURLtype && (
357
- <CapRow type="flex" align="middle">
358
- <CapRow type="flex" align="middle" justify="start">
359
- <CapLabel className="url-type" type="label2">
360
- {button?.urlType === STATIC_URL
361
- ? formatMessage(capWhatsappCTAMsg.ctaWebsiteTypeStatic)
362
- : formatMessage(capWhatsappCTAMsg.ctaWebsiteTypeDynamic)}
363
- </CapLabel>
364
- </CapRow>
365
- <CapRow type='flex' className="whatsapp-url-preview-wrapper">
366
- <CapTooltip title={button?.url} placement="top">
367
- <CapColumn span={16} className="margin-r-0">
368
- <CapLabel className="url">{button?.url}</CapLabel>
369
- </CapColumn>
370
- </CapTooltip>
364
+ )}
365
+ {isURLtype && (
366
+ <CapTooltip
367
+ title={button?.url}
368
+ placement="top"
369
+ wrapperClassName="whatsapp-cta-url-tooltip-wrapper"
370
+ triggerClassName="whatsapp-cta-url-tooltip-trigger"
371
+ >
372
+ <CapLabel className="url">{button?.url}</CapLabel>
373
+ </CapTooltip>
374
+ )}
375
+ </CapColumn>
376
+ <CapColumn className="whatsapp-saved-cta-cell whatsapp-saved-cta-actions-cell">
377
+ {!isEditFlow && (
378
+ <CapRow
379
+ className="whatsapp-saved-cta-action-icons"
380
+ type="flex"
381
+ align="middle"
382
+ noWrap
383
+ >
384
+ <CapIcon size="s" type="edit" onClick={() => handleEditButton(index)}/>
385
+ {parseInt(carouselIndex, 10) === 0 && (
386
+ <CapIcon size="s" type="delete" onClick={() => handleDeleteButton(index)} disabled={buttonData?.length === 1}/>
387
+ )}
371
388
  </CapRow>
372
- </CapRow>
373
- )}
374
- {!isEditFlow && (
375
- <CapRow type='flex' align='middle' justify="end" gap={12} fillSpace>
376
- <CapIcon size="s" type="edit" onClick={() => handleEditButton(index)}/>
377
- {parseInt(carouselIndex, 10) === 0 && (
378
- <CapIcon size="s" type="delete" onClick={() => handleDeleteButton(index)} disabled={buttonData?.length === 1}/>
379
- )}
380
- </CapRow>
381
- )}
389
+ )}
390
+ </CapColumn>
382
391
  {isEditFlow && button?.urlType === DYNAMIC_URL && button?.url.includes('{{1}}') && (
383
392
  <CapColumn>
384
393
  <TagList